reeval 0.7.0 → 0.9.0
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/README.md +12 -12
- package/dist/{agent-Dhd5FJCZ.js → agent-6FefIhX7.js} +6 -6
- package/dist/{agent-Dhd5FJCZ.js.map → agent-6FefIhX7.js.map} +1 -1
- package/dist/{agent-gjBiZis8.js → agent-WSmTaesf.js} +52 -17
- package/dist/agent-WSmTaesf.js.map +1 -0
- package/dist/{analysis-BV3TCznv.js → analysis-DlGBDjTj.js} +4 -4
- package/dist/{analysis-BV3TCznv.js.map → analysis-DlGBDjTj.js.map} +1 -1
- package/dist/{assemble-Dd_mZYnu.js → assemble-BCZzTyR7.js} +259 -18
- package/dist/assemble-BCZzTyR7.js.map +1 -0
- package/dist/assemble-C5qq5yT2.js +2 -0
- package/dist/capture-Bp74Z6Fr.js +1257 -0
- package/dist/capture-Bp74Z6Fr.js.map +1 -0
- package/dist/capture-DS-cPyuE.js +2 -0
- package/dist/{cli-r9CDqoFL.js → cli-CVrMh_qz.js} +210 -112
- package/dist/cli-CVrMh_qz.js.map +1 -0
- package/dist/{cli-spec-DOyxAMc2.js → cli-spec-CKEYa8V2.js} +6 -5
- package/dist/cli-spec-CKEYa8V2.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{client-LsfDiD-X.js → client-Bbo9SPlK.js} +31 -201
- package/dist/client-Bbo9SPlK.js.map +1 -0
- package/dist/{command-l4ZFCeMh.js → command-BFP0hKEb.js} +7 -7
- package/dist/{command-l4ZFCeMh.js.map → command-BFP0hKEb.js.map} +1 -1
- package/dist/command-BrmXUC-W.js +2 -0
- package/dist/{command-DMR69l5G.js → command-C5xFPKHM.js} +21 -20
- package/dist/command-C5xFPKHM.js.map +1 -0
- package/dist/{command-Cej2t3y-.js → command-CzIFp1JQ.js} +10 -10
- package/dist/{command-Cej2t3y-.js.map → command-CzIFp1JQ.js.map} +1 -1
- package/dist/{command-D6r2pwO5.js → command-DUT5l13I.js} +221 -54
- package/dist/command-DUT5l13I.js.map +1 -0
- package/dist/{commands-BA_NCh2h.js → commands-CLmPBrTS.js} +6 -6
- package/dist/{commands-BA_NCh2h.js.map → commands-CLmPBrTS.js.map} +1 -1
- package/dist/{commands-DgOUkcSO.js → commands-fI3G2Wnh.js} +1 -1
- package/dist/{config-DuQDCx0B.js → config-CGvBeewH.js} +18 -8
- package/dist/config-CGvBeewH.js.map +1 -0
- package/dist/{context-CvUv7DUb.js → context-DbSMEVKs.js} +1 -5
- package/dist/{context-CvUv7DUb.js.map → context-DbSMEVKs.js.map} +1 -1
- package/dist/{db-o_osRkqr.js → db-CSSFtGcN.js} +1 -1
- package/dist/{db-CjX7JF4o.js → db-CfQFwJtq.js} +209 -40
- package/dist/db-CfQFwJtq.js.map +1 -0
- package/dist/{docker-DaUaG4XZ.js → docker-CRQGQbIt.js} +17 -5
- package/dist/docker-CRQGQbIt.js.map +1 -0
- package/dist/{docker-ZaYeKE2w.js → docker-DV2GNgle.js} +2 -2
- package/dist/{docker-ZaYeKE2w.js.map → docker-DV2GNgle.js.map} +1 -1
- package/dist/{docker-Dnya8eo9.js → docker-Df_YOxxB.js} +1 -1
- package/dist/{doctor-Bk6aJeec.js → doctor-DlKycx5H.js} +13 -13
- package/dist/{doctor-Bk6aJeec.js.map → doctor-DlKycx5H.js.map} +1 -1
- package/dist/{egress-BKbyriEa.js → egress-BhzNh7Yi.js} +18 -10
- package/dist/egress-BhzNh7Yi.js.map +1 -0
- package/dist/{environment-DiCljXMD.js → environment-1jE-HhO-.js} +2 -2
- package/dist/{environment-DiCljXMD.js.map → environment-1jE-HhO-.js.map} +1 -1
- package/dist/fake-instructions-CUI33I_F.js +32 -0
- package/dist/fake-instructions-CUI33I_F.js.map +1 -0
- package/dist/family-D2b7WD10.js +67 -0
- package/dist/family-D2b7WD10.js.map +1 -0
- package/dist/files-AiEKuEZe.js +2 -0
- package/dist/files-BMCvfB4X.js +2 -0
- package/dist/{files-TNrZJQ3Y.js → files-CSzVADHn.js} +5 -5
- package/dist/{files-TNrZJQ3Y.js.map → files-CSzVADHn.js.map} +1 -1
- package/dist/{files-BSMSnQMa.js → files-DJUFg-4e.js} +3 -3
- package/dist/{files-BSMSnQMa.js.map → files-DJUFg-4e.js.map} +1 -1
- package/dist/{git-DNGtrOg9.js → git-DfG-YJ1u.js} +2 -2
- package/dist/{git-DNGtrOg9.js.map → git-DfG-YJ1u.js.map} +1 -1
- package/dist/{harness-CDRaaGvR.js → harness-CeCp2GSW.js} +52 -12
- package/dist/harness-CeCp2GSW.js.map +1 -0
- package/dist/harness.d.ts +26 -13
- package/dist/harness.js +1 -1
- package/dist/{header-BZF8Xss1.js → header-CALhmafp.js} +10 -3
- package/dist/{header-BZF8Xss1.js.map → header-CALhmafp.js.map} +1 -1
- package/dist/{images-BpwFDT1E.js → images-BMgdpMAX.js} +6 -6
- package/dist/{images-BpwFDT1E.js.map → images-BMgdpMAX.js.map} +1 -1
- package/dist/index.d.ts +17 -5
- package/dist/index.js +165 -67
- package/dist/index.js.map +1 -1
- package/dist/init-B6vegaP_.js +2 -0
- package/dist/{init-CBWt1rZT.js → init-BIdLF0F9.js} +4 -4
- package/dist/{init-CBWt1rZT.js.map → init-BIdLF0F9.js.map} +1 -1
- package/dist/{invocations-DArYWyXN.js → invocations-CLaL8AuW.js} +3 -3
- package/dist/{invocations-DArYWyXN.js.map → invocations-CLaL8AuW.js.map} +1 -1
- package/dist/{judge-7WjNdU9z.js → judge-CeYPN8P_.js} +2 -2
- package/dist/{judge-7WjNdU9z.js.map → judge-CeYPN8P_.js.map} +1 -1
- package/dist/judge.d.ts +5 -4
- package/dist/judge.js +4 -3
- package/dist/judge.js.map +1 -1
- package/dist/{live-DZkCtmNP.js → live-S0qODb6o.js} +2 -2
- package/dist/{live-DZkCtmNP.js.map → live-S0qODb6o.js.map} +1 -1
- package/dist/{model-Cp6NoAT3.js → model-B1BZoZ9b.js} +9 -8
- package/dist/model-B1BZoZ9b.js.map +1 -0
- package/dist/{model-DyQbu7KF.js → model-BOnlAPzs.js} +288 -38
- package/dist/model-BOnlAPzs.js.map +1 -0
- package/dist/{model-Bv8uUZ07.js → model-DEQo3FkZ.js} +6 -6
- package/dist/model-DEQo3FkZ.js.map +1 -0
- package/dist/model-DWeg5J7z.js +2 -0
- package/dist/{model-BeoRxCcJ.js → model-FXko2kl7.js} +1 -1
- package/dist/{options-Ge_x32XE.js → options-CKqyPGxX.js} +161 -34
- package/dist/options-CKqyPGxX.js.map +1 -0
- package/dist/pairs-DDsjgm5F.js +149 -0
- package/dist/pairs-DDsjgm5F.js.map +1 -0
- package/dist/{patch-CGCVipPu.js → patch-bDV8YFW0.js} +2 -2
- package/dist/{patch-CGCVipPu.js.map → patch-bDV8YFW0.js.map} +1 -1
- package/dist/{plan-BqBadHYj.js → plan-DUydFc_1.js} +69 -31
- package/dist/plan-DUydFc_1.js.map +1 -0
- package/dist/{preflight-CyaAZE9r.js → preflight-BkoSxf4C.js} +3 -3
- package/dist/{preflight-CyaAZE9r.js.map → preflight-BkoSxf4C.js.map} +1 -1
- package/dist/{preflight-CFtZNTLt.js → preflight-D5LW7Oqi.js} +1 -1
- package/dist/{prepare-Zr1xOMUX.js → prepare-CUAWHebF.js} +5 -5
- package/dist/{prepare-Zr1xOMUX.js.map → prepare-CUAWHebF.js.map} +1 -1
- package/dist/{prepare-Dl7-t3fO.js → prepare-oji2oO8W.js} +2 -2
- package/dist/process-CSVO_jsc.js +119 -0
- package/dist/process-CSVO_jsc.js.map +1 -0
- package/dist/{provenance-Byl2T6LJ.js → provenance-D7HK3vzt.js} +2 -2
- package/dist/{provenance-Byl2T6LJ.js.map → provenance-D7HK3vzt.js.map} +1 -1
- package/dist/{providers-Cflw7Ks-.js → providers-B1DzMvmH.js} +3 -3
- package/dist/{providers-Cflw7Ks-.js.map → providers-B1DzMvmH.js.map} +1 -1
- package/dist/{providers-SBMExI53.js → providers-CpEfNU1F.js} +1 -1
- package/dist/{query-DUSLlic0.js → query-BjBMLa-C.js} +2 -2
- package/dist/{query-DUSLlic0.js.map → query-BjBMLa-C.js.map} +1 -1
- package/dist/query-oPO_XrTy.js +2 -0
- package/dist/{questions-ZdT3JDyQ.js → questions-B8by4Bbt.js} +28 -119
- package/dist/questions-B8by4Bbt.js.map +1 -0
- package/dist/{record-CbuQH4l1.js → record-BRP9vZrj.js} +21 -1
- package/dist/record-BRP9vZrj.js.map +1 -0
- package/dist/{results-owJR-NFm.js → results-Di7m8dQ3.js} +4 -4
- package/dist/{results-owJR-NFm.js.map → results-Di7m8dQ3.js.map} +1 -1
- package/dist/{resume-BL4Yw7Ld.js → resume-SUz1GskV.js} +4 -4
- package/dist/{resume-BL4Yw7Ld.js.map → resume-SUz1GskV.js.map} +1 -1
- package/dist/runtime-DdsfnRz_.js +344 -0
- package/dist/runtime-DdsfnRz_.js.map +1 -0
- package/dist/{sandbox--ZXuscAZ.js → sandbox-D5v-of6d.js} +10 -5
- package/dist/sandbox-D5v-of6d.js.map +1 -0
- package/dist/sandbox.d.ts +4 -3
- package/dist/sandbox.js +1 -1
- package/dist/{server-yFVUxAm0.js → server-BNnXtmS8.js} +9 -9
- package/dist/{server-yFVUxAm0.js.map → server-BNnXtmS8.js.map} +1 -1
- package/dist/server-BNwoKzlm.js +2 -0
- package/dist/source-Bk_0ZaEY.js +2 -0
- package/dist/{source-M3gS517R.js → source-Dof0NQBm.js} +2 -2
- package/dist/{source-M3gS517R.js.map → source-Dof0NQBm.js.map} +1 -1
- package/dist/{src-79JhW-zG.js → src-8rRXy4u9.js} +71 -124
- package/dist/src-8rRXy4u9.js.map +1 -0
- package/dist/src-BGWuWp6I.js +3 -0
- package/dist/src-BRt0O02i.js +3 -0
- package/dist/src-Bx2x1ekD.js +3 -0
- package/dist/{src-s-VffoYK.js → src-C1E9HDWO.js} +5 -3
- package/dist/{src-s-VffoYK.js.map → src-C1E9HDWO.js.map} +1 -1
- package/dist/{src-IOr1rhEg.js → src-COgE7991.js} +10 -8
- package/dist/{src-IOr1rhEg.js.map → src-COgE7991.js.map} +1 -1
- package/dist/{src-CokPbYCb.js → src-C_4Umgh2.js} +23 -14
- package/dist/{src-CokPbYCb.js.map → src-C_4Umgh2.js.map} +1 -1
- package/dist/{src-DxFJrMUA.js → src-D95O-Smv.js} +12 -7
- package/dist/{src-DxFJrMUA.js.map → src-D95O-Smv.js.map} +1 -1
- package/dist/{src-DTcalvuF.js → src-DDNXsD9H.js} +28 -4
- package/dist/{src-DTcalvuF.js.map → src-DDNXsD9H.js.map} +1 -1
- package/dist/src-DLa-ESW0.js +4 -0
- package/dist/{src-DSE0FWaU.js → src-DVy58-zZ.js} +2 -2
- package/dist/{src-DSE0FWaU.js.map → src-DVy58-zZ.js.map} +1 -1
- package/dist/src-cN0aB-WR.js +3 -0
- package/dist/{src-DH0jhpoi.js → src-cdIVjC5b.js} +1 -1
- package/dist/src-sIsDHWYq.js +3 -0
- package/dist/state-Bo5zg4da.js +2 -0
- package/dist/{state-DEj_utaE.js → state-CiXyLqg0.js} +10 -10
- package/dist/{state-DEj_utaE.js.map → state-CiXyLqg0.js.map} +1 -1
- package/dist/studio-app/assets/index-B9MCTfuu.css +2 -0
- package/dist/studio-app/assets/{index-Bbl0oTxu.js → index-CROH2NB3.js} +12 -12
- package/dist/studio-app/index.html +2 -2
- package/dist/{summary-D012KlUM.js → summary-2OAWF2N7.js} +7 -7
- package/dist/{summary-D012KlUM.js.map → summary-2OAWF2N7.js.map} +1 -1
- package/dist/{summary--FBuB_sr.js → summary-BTksrIxn.js} +63 -5
- package/dist/summary-BTksrIxn.js.map +1 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +2 -0
- package/dist/{tools-CJHw-zCo.js → tools-B10Uj7OI.js} +4 -4
- package/dist/tools-B10Uj7OI.js.map +1 -0
- package/dist/types-C5vI3g1C.js.map +1 -1
- package/dist/{types-BsRSknLe.d.ts → types-D8RLfkFP.d.ts} +453 -42
- package/dist/{usage-DsYUqIUM.js → usage-DDF_6tkV.js} +4 -2
- package/dist/{usage-DsYUqIUM.js.map → usage-DDF_6tkV.js.map} +1 -1
- package/dist/view/index.d.ts +60 -15
- package/dist/view/index.js +7 -6
- package/dist/views-Cs2Oqayd.js +3 -0
- package/dist/{views-jhL9Jb8A.js → views-Dqa5ojfZ.js} +4 -4
- package/dist/{views-jhL9Jb8A.js.map → views-Dqa5ojfZ.js.map} +1 -1
- package/dist/{worker-entry-OwnsrUdr.js → worker-entry-BAikHl9F.js} +9 -4
- package/dist/worker-entry-BAikHl9F.js.map +1 -0
- package/dist/{workspace-C4_J0pqb.js → workspace-C-TrNIUc.js} +4 -4
- package/dist/{workspace-C4_J0pqb.js.map → workspace-C-TrNIUc.js.map} +1 -1
- package/dist/{workspace-wudSz_W-.js → workspace-Do_SiWBs.js} +1 -1
- package/docker/Dockerfile +7 -5
- package/package.json +9 -4
- package/dist/agent-gjBiZis8.js.map +0 -1
- package/dist/assemble-D8sZ4Jiv.js +0 -2
- package/dist/assemble-Dd_mZYnu.js.map +0 -1
- package/dist/capture-CisAmy-G.js +0 -587
- package/dist/capture-CisAmy-G.js.map +0 -1
- package/dist/capture-DquyqAHC.js +0 -2
- package/dist/cli-r9CDqoFL.js.map +0 -1
- package/dist/cli-spec-DOyxAMc2.js.map +0 -1
- package/dist/client-LsfDiD-X.js.map +0 -1
- package/dist/command-Cxfgghqo.js +0 -2
- package/dist/command-D6r2pwO5.js.map +0 -1
- package/dist/command-DMR69l5G.js.map +0 -1
- package/dist/config-DuQDCx0B.js.map +0 -1
- package/dist/db-CjX7JF4o.js.map +0 -1
- package/dist/docker-DaUaG4XZ.js.map +0 -1
- package/dist/egress-BKbyriEa.js.map +0 -1
- package/dist/files-DmZGPsWK.js +0 -2
- package/dist/files-Tb8wY0Ze.js +0 -2
- package/dist/harness-CDRaaGvR.js.map +0 -1
- package/dist/init-1THeGfut.js +0 -2
- package/dist/model-Bv8uUZ07.js.map +0 -1
- package/dist/model-Cp6NoAT3.js.map +0 -1
- package/dist/model-D_pF3Vcl.js +0 -2
- package/dist/model-DyQbu7KF.js.map +0 -1
- package/dist/options-Ge_x32XE.js.map +0 -1
- package/dist/plan-BqBadHYj.js.map +0 -1
- package/dist/query-CvutWHaL.js +0 -2
- package/dist/questions-ZdT3JDyQ.js.map +0 -1
- package/dist/record-CbuQH4l1.js.map +0 -1
- package/dist/sandbox--ZXuscAZ.js.map +0 -1
- package/dist/server-CNPsB0cT.js +0 -2
- package/dist/source-Cmc5SKFn.js +0 -2
- package/dist/src--L8B4WGa.js +0 -3
- package/dist/src-79JhW-zG.js.map +0 -1
- package/dist/src-BLN459er.js +0 -3
- package/dist/src-Bz66jDtE.js +0 -3
- package/dist/src-CfUwg9UY.js +0 -3
- package/dist/src-DV_3QDHA.js +0 -4
- package/dist/src-Ljh2vKXm.js +0 -3
- package/dist/state-DkqbCVVp.js +0 -2
- package/dist/studio-app/assets/index-CUx6GoL5.css +0 -2
- package/dist/summary--FBuB_sr.js.map +0 -1
- package/dist/tools-CJHw-zCo.js.map +0 -1
- package/dist/views-yPmmoo8f.js +0 -3
- package/dist/worker-entry-OwnsrUdr.js.map +0 -1
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
import { t as parseDuration } from "./duration-JsJrIWXq.js";
|
|
2
|
+
import { s as cacheDir } from "./root-CvdRlRZX.js";
|
|
3
|
+
import { t as InfraError } from "./types-C5vI3g1C.js";
|
|
4
|
+
import { l as sha256 } from "./ids-Bp6ut7ZD.js";
|
|
5
|
+
import { l as judgeRoute } from "./judge-CeYPN8P_.js";
|
|
6
|
+
import { n as shellQuote } from "./process-CSVO_jsc.js";
|
|
7
|
+
import { n as getRunContext, o as SUBJECT } from "./runtime-DdsfnRz_.js";
|
|
8
|
+
import { i as debugTrace, r as debugLog } from "./attempt-CGPV3BZY.js";
|
|
9
|
+
import { n as clip, r as formatDuration } from "./text-gEcKjAyp.js";
|
|
10
|
+
import { a as providerModel } from "./model-B1BZoZ9b.js";
|
|
11
|
+
import { r as isWaitStep, t as fakeInstructionSteps } from "./fake-instructions-CUI33I_F.js";
|
|
12
|
+
import { t as addUsage } from "./usage-DDF_6tkV.js";
|
|
13
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
|
+
import { join, posix } from "node:path";
|
|
16
|
+
import { createServer } from "node:net";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
20
|
+
import { generateText, stepCountIs, tool } from "ai";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { crc32, deflateSync, inflateRawSync } from "node:zlib";
|
|
23
|
+
//#region src/capture/zip.ts
|
|
24
|
+
const LOCAL = 67324752;
|
|
25
|
+
/** Extracts a zip (store or deflate) into `files` keyed by posix path. */
|
|
26
|
+
function unzip(bytes) {
|
|
27
|
+
const buf = Buffer.from(bytes);
|
|
28
|
+
const files = /* @__PURE__ */ new Map();
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i + 30 <= buf.length) {
|
|
31
|
+
if (buf.readUInt32LE(i) !== LOCAL) break;
|
|
32
|
+
const method = buf.readUInt16LE(i + 8);
|
|
33
|
+
const comp = buf.readUInt32LE(i + 18);
|
|
34
|
+
const nameLen = buf.readUInt16LE(i + 26);
|
|
35
|
+
const extra = buf.readUInt16LE(i + 28);
|
|
36
|
+
const name = buf.toString("utf8", i + 30, i + 30 + nameLen);
|
|
37
|
+
const start = i + 30 + nameLen + extra;
|
|
38
|
+
const raw = buf.subarray(start, start + comp);
|
|
39
|
+
if (name.endsWith("/")) {
|
|
40
|
+
i = start + comp;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (method === 0) files.set(name, Buffer.from(raw));
|
|
44
|
+
else if (method === 8) files.set(name, Buffer.from(inflateRawSync(raw)));
|
|
45
|
+
else throw new Error(`zip method ${method} is not supported (${name})`);
|
|
46
|
+
i = start + comp;
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/capture/browser.ts
|
|
52
|
+
const isMs = (wait) => typeof wait === "object" && wait !== null;
|
|
53
|
+
/** The CDP event `'idle'` waits for; `'load'` is `Page.loadEventFired`. */
|
|
54
|
+
function captureWaitLabel(wait) {
|
|
55
|
+
return wait === "idle" ? "networkAlmostIdle" : "load";
|
|
56
|
+
}
|
|
57
|
+
function captureWaitTimeout(wait) {
|
|
58
|
+
return /* @__PURE__ */ new Error(`c.capture() timed out waiting for ${captureWaitLabel(wait)}`);
|
|
59
|
+
}
|
|
60
|
+
function waitTimeout(wait, detail) {
|
|
61
|
+
if (isMs(wait)) return /* @__PURE__ */ new Error(`c.capture() timed out${detail ? ` on ${detail}` : ""} waiting ${wait.ms}ms`);
|
|
62
|
+
return captureWaitTimeout(wait);
|
|
63
|
+
}
|
|
64
|
+
async function waitForPage(cdp, wait, timeoutMs, nav) {
|
|
65
|
+
if (isMs(wait)) {
|
|
66
|
+
if (timeoutMs <= 0) throw waitTimeout(wait);
|
|
67
|
+
const ms = Math.min(wait.ms, timeoutMs);
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const page = listenForCaptureWait(cdp, wait);
|
|
72
|
+
page.arm();
|
|
73
|
+
await page.ready(nav, timeoutMs);
|
|
74
|
+
}
|
|
75
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
76
|
+
/** Map a full-page click onto the live window, scrolling with the wheel if needed. */
|
|
77
|
+
function mapPagePoint(x, y, window, page, scrollY) {
|
|
78
|
+
const maxY = Math.max(0, (page?.height ?? window.height) - window.height);
|
|
79
|
+
const target = y >= window.height || y < 0 ? clamp(y - window.height / 2, 0, maxY) : scrollY;
|
|
80
|
+
const nextY = clamp(target, 0, maxY);
|
|
81
|
+
return {
|
|
82
|
+
x: clamp(x, 0, window.width - 1),
|
|
83
|
+
y: clamp(y - nextY, 0, window.height - 1),
|
|
84
|
+
scrollY: nextY,
|
|
85
|
+
dy: nextY - scrollY
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function dispatchInput(cdp, action, window, page, scrollY = 0) {
|
|
89
|
+
if (action.type === "done") return scrollY;
|
|
90
|
+
if (action.type === "type") {
|
|
91
|
+
await cdp.send("Input.insertText", { text: action.text });
|
|
92
|
+
return scrollY;
|
|
93
|
+
}
|
|
94
|
+
if (action.type === "key") {
|
|
95
|
+
const key = action.key;
|
|
96
|
+
const vk = key === "Tab" ? 9 : 13;
|
|
97
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
98
|
+
type: "keyDown",
|
|
99
|
+
key,
|
|
100
|
+
code: key,
|
|
101
|
+
windowsVirtualKeyCode: vk
|
|
102
|
+
});
|
|
103
|
+
await cdp.send("Input.dispatchKeyEvent", {
|
|
104
|
+
type: "keyUp",
|
|
105
|
+
key,
|
|
106
|
+
code: key,
|
|
107
|
+
windowsVirtualKeyCode: vk
|
|
108
|
+
});
|
|
109
|
+
return scrollY;
|
|
110
|
+
}
|
|
111
|
+
if (action.type === "scroll") {
|
|
112
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
113
|
+
type: "mouseWheel",
|
|
114
|
+
x: action.x,
|
|
115
|
+
y: action.y,
|
|
116
|
+
deltaX: 0,
|
|
117
|
+
deltaY: action.dy
|
|
118
|
+
});
|
|
119
|
+
return scrollY + action.dy;
|
|
120
|
+
}
|
|
121
|
+
const mapped = mapPagePoint(action.type === "drag" ? action.x : action.x, action.type === "drag" ? action.y : action.y, window, page, scrollY);
|
|
122
|
+
if (mapped.dy !== 0) await cdp.send("Input.dispatchMouseEvent", {
|
|
123
|
+
type: "mouseWheel",
|
|
124
|
+
x: window.width / 2,
|
|
125
|
+
y: window.height / 2,
|
|
126
|
+
deltaX: 0,
|
|
127
|
+
deltaY: mapped.dy
|
|
128
|
+
});
|
|
129
|
+
const at = {
|
|
130
|
+
x: mapped.x,
|
|
131
|
+
y: mapped.y
|
|
132
|
+
};
|
|
133
|
+
if (action.type === "hover") {
|
|
134
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
135
|
+
type: "mouseMoved",
|
|
136
|
+
...at
|
|
137
|
+
});
|
|
138
|
+
return mapped.scrollY;
|
|
139
|
+
}
|
|
140
|
+
if (action.type === "click") {
|
|
141
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
142
|
+
type: "mouseMoved",
|
|
143
|
+
...at,
|
|
144
|
+
button: "left"
|
|
145
|
+
});
|
|
146
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
147
|
+
type: "mousePressed",
|
|
148
|
+
...at,
|
|
149
|
+
button: "left",
|
|
150
|
+
clickCount: 1
|
|
151
|
+
});
|
|
152
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
153
|
+
type: "mouseReleased",
|
|
154
|
+
...at,
|
|
155
|
+
button: "left",
|
|
156
|
+
clickCount: 1
|
|
157
|
+
});
|
|
158
|
+
return mapped.scrollY;
|
|
159
|
+
}
|
|
160
|
+
const end = mapPagePoint(action.toX, action.toY, window, page, mapped.scrollY);
|
|
161
|
+
if (end.dy !== 0) await cdp.send("Input.dispatchMouseEvent", {
|
|
162
|
+
type: "mouseWheel",
|
|
163
|
+
x: window.width / 2,
|
|
164
|
+
y: window.height / 2,
|
|
165
|
+
deltaX: 0,
|
|
166
|
+
deltaY: end.dy
|
|
167
|
+
});
|
|
168
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
169
|
+
type: "mouseMoved",
|
|
170
|
+
x: at.x,
|
|
171
|
+
y: at.y,
|
|
172
|
+
button: "left"
|
|
173
|
+
});
|
|
174
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
175
|
+
type: "mousePressed",
|
|
176
|
+
x: at.x,
|
|
177
|
+
y: at.y,
|
|
178
|
+
button: "left",
|
|
179
|
+
clickCount: 1
|
|
180
|
+
});
|
|
181
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
182
|
+
type: "mouseMoved",
|
|
183
|
+
x: end.x,
|
|
184
|
+
y: end.y,
|
|
185
|
+
button: "left"
|
|
186
|
+
});
|
|
187
|
+
await cdp.send("Input.dispatchMouseEvent", {
|
|
188
|
+
type: "mouseReleased",
|
|
189
|
+
x: end.x,
|
|
190
|
+
y: end.y,
|
|
191
|
+
button: "left",
|
|
192
|
+
clickCount: 1
|
|
193
|
+
});
|
|
194
|
+
return end.scrollY;
|
|
195
|
+
}
|
|
196
|
+
function lifecycleParams(msg) {
|
|
197
|
+
const body = msg;
|
|
198
|
+
return body.params ?? body;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Arms after `Page.enable` so about:blank's events cannot resolve the wait.
|
|
202
|
+
* Call `arm` immediately before `Page.navigate`.
|
|
203
|
+
*/
|
|
204
|
+
function listenForCaptureWait(cdp, wait) {
|
|
205
|
+
const method = wait === "idle" ? "Page.lifecycleEvent" : "Page.loadEventFired";
|
|
206
|
+
const pending = [];
|
|
207
|
+
let deliver;
|
|
208
|
+
let started = false;
|
|
209
|
+
cdp.on(method, (msg) => {
|
|
210
|
+
if (!started) return;
|
|
211
|
+
if (deliver) deliver(msg);
|
|
212
|
+
else pending.push(msg);
|
|
213
|
+
});
|
|
214
|
+
const matches = (msg, loaderId) => {
|
|
215
|
+
if (wait === "load") return true;
|
|
216
|
+
const params = lifecycleParams(msg);
|
|
217
|
+
if (params.name !== "networkAlmostIdle") return false;
|
|
218
|
+
return !loaderId || params.loaderId === loaderId;
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
arm() {
|
|
222
|
+
started = true;
|
|
223
|
+
},
|
|
224
|
+
ready(nav, timeoutMs) {
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const timer = setTimeout(() => reject(captureWaitTimeout(wait)), timeoutMs);
|
|
227
|
+
const finish = () => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
resolve();
|
|
230
|
+
};
|
|
231
|
+
deliver = (msg) => {
|
|
232
|
+
if (matches(msg, nav?.loaderId)) finish();
|
|
233
|
+
};
|
|
234
|
+
if (pending.some((msg) => matches(msg, nav?.loaderId))) finish();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Pinned Chrome for Testing `chrome-headless-shell` build. */
|
|
240
|
+
const CHROME_VERSION = "140.0.7339.185";
|
|
241
|
+
const CHROME_NAME = "chrome-headless-shell";
|
|
242
|
+
function chromePlatform() {
|
|
243
|
+
if (process.platform === "darwin") return process.arch === "arm64" ? "mac-arm64" : "mac-x64";
|
|
244
|
+
if (process.platform === "win32") return "win64";
|
|
245
|
+
return "linux64";
|
|
246
|
+
}
|
|
247
|
+
function chromeUrl(platform) {
|
|
248
|
+
return `https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/${platform}/${CHROME_NAME}-${platform}.zip`;
|
|
249
|
+
}
|
|
250
|
+
function binaryName() {
|
|
251
|
+
return process.platform === "win32" ? `${CHROME_NAME}.exe` : CHROME_NAME;
|
|
252
|
+
}
|
|
253
|
+
async function downloadZip(url, onProgress, cacheLabel, signal) {
|
|
254
|
+
const res = await fetch(url, { signal });
|
|
255
|
+
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
|
|
256
|
+
const total = Number(res.headers.get("content-length")) || null;
|
|
257
|
+
const report = (received) => onProgress?.({
|
|
258
|
+
name: CHROME_NAME,
|
|
259
|
+
version: CHROME_VERSION,
|
|
260
|
+
bytes: total,
|
|
261
|
+
received,
|
|
262
|
+
cache: cacheLabel
|
|
263
|
+
});
|
|
264
|
+
report(0);
|
|
265
|
+
if (!res.body) return Buffer.from(await res.arrayBuffer());
|
|
266
|
+
const reader = res.body.getReader();
|
|
267
|
+
const chunks = [];
|
|
268
|
+
let received = 0;
|
|
269
|
+
let last = 0;
|
|
270
|
+
for (;;) {
|
|
271
|
+
const { done, value } = await reader.read();
|
|
272
|
+
if (done) break;
|
|
273
|
+
chunks.push(value);
|
|
274
|
+
received += value.length;
|
|
275
|
+
if (received - last >= 262144 || total && received === total) {
|
|
276
|
+
report(received);
|
|
277
|
+
last = received;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (received !== last) report(received);
|
|
281
|
+
return Buffer.concat(chunks);
|
|
282
|
+
}
|
|
283
|
+
function extractBinary(zip, dest, platform) {
|
|
284
|
+
const files = unzip(zip);
|
|
285
|
+
const needle = `/${binaryName()}`;
|
|
286
|
+
const entry = [...files.entries()].find(([name]) => name.endsWith(needle) || name === binaryName() || name.endsWith(`\\${binaryName()}`));
|
|
287
|
+
if (!entry) {
|
|
288
|
+
const names = [...files.keys()].slice(0, 8).join(", ");
|
|
289
|
+
throw new Error(`zip has no ${binaryName()} (saw ${names || "nothing"})`);
|
|
290
|
+
}
|
|
291
|
+
const prefix = entry[0].slice(0, -binaryName().length);
|
|
292
|
+
mkdirSync(dest, { recursive: true });
|
|
293
|
+
for (const [name, data] of files) {
|
|
294
|
+
const rel = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
295
|
+
if (!rel || rel.endsWith("/")) continue;
|
|
296
|
+
const target = join(dest, ...rel.split("/"));
|
|
297
|
+
mkdirSync(join(target, ".."), { recursive: true });
|
|
298
|
+
writeFileSync(target, data);
|
|
299
|
+
}
|
|
300
|
+
const bin = join(dest, binaryName());
|
|
301
|
+
if (!existsSync(bin)) writeFileSync(bin, entry[1]);
|
|
302
|
+
if (process.platform !== "win32") chmodSync(bin, 493);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Returns the cached binary, downloading on a miss. `onProgress` runs only for a download.
|
|
306
|
+
*/
|
|
307
|
+
async function ensureChrome(cacheRoot, options = { cacheLabel: cacheRoot }) {
|
|
308
|
+
const platform = chromePlatform();
|
|
309
|
+
const dest = join(cacheRoot, CHROME_NAME, CHROME_VERSION, platform);
|
|
310
|
+
const bin = join(dest, binaryName());
|
|
311
|
+
if (existsSync(bin)) return bin;
|
|
312
|
+
mkdirSync(join(cacheRoot, CHROME_NAME, CHROME_VERSION), { recursive: true });
|
|
313
|
+
const partial = `${dest}.${process.pid}.partial`;
|
|
314
|
+
rmSync(partial, {
|
|
315
|
+
recursive: true,
|
|
316
|
+
force: true
|
|
317
|
+
});
|
|
318
|
+
try {
|
|
319
|
+
extractBinary(await downloadZip(chromeUrl(platform), options.onProgress, options.cacheLabel, options.signal), partial, platform);
|
|
320
|
+
try {
|
|
321
|
+
renameSync(partial, dest);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
rmSync(partial, {
|
|
324
|
+
recursive: true,
|
|
325
|
+
force: true
|
|
326
|
+
});
|
|
327
|
+
if (!existsSync(bin)) throw error;
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
rmSync(partial, {
|
|
331
|
+
recursive: true,
|
|
332
|
+
force: true
|
|
333
|
+
});
|
|
334
|
+
if (existsSync(bin)) return bin;
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
return bin;
|
|
338
|
+
}
|
|
339
|
+
var Cdp = class {
|
|
340
|
+
#ws;
|
|
341
|
+
#next = 0;
|
|
342
|
+
#pending = /* @__PURE__ */ new Map();
|
|
343
|
+
#events = /* @__PURE__ */ new Map();
|
|
344
|
+
constructor(ws) {
|
|
345
|
+
this.#ws = ws;
|
|
346
|
+
ws.addEventListener("message", (ev) => {
|
|
347
|
+
const msg = JSON.parse(String(ev.data));
|
|
348
|
+
if (msg.id !== void 0) {
|
|
349
|
+
const p = this.#pending.get(msg.id);
|
|
350
|
+
if (!p) return;
|
|
351
|
+
this.#pending.delete(msg.id);
|
|
352
|
+
if (msg.error) p.reject(new Error(msg.error.message));
|
|
353
|
+
else p.resolve(msg.result);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (msg.method) for (const fn of this.#events.get(msg.method) ?? []) fn(msg);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
on(method, fn) {
|
|
360
|
+
const list = this.#events.get(method) ?? [];
|
|
361
|
+
list.push(fn);
|
|
362
|
+
this.#events.set(method, list);
|
|
363
|
+
}
|
|
364
|
+
send(method, params) {
|
|
365
|
+
const id = ++this.#next;
|
|
366
|
+
this.#ws.send(JSON.stringify({
|
|
367
|
+
id,
|
|
368
|
+
method,
|
|
369
|
+
params
|
|
370
|
+
}));
|
|
371
|
+
return new Promise((resolve, reject) => this.#pending.set(id, {
|
|
372
|
+
resolve,
|
|
373
|
+
reject
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
close() {
|
|
377
|
+
this.#ws.close();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
function waitForPortFile(file, timeoutMs) {
|
|
381
|
+
const deadline = Date.now() + timeoutMs;
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
const tick = () => {
|
|
384
|
+
if (existsSync(file)) {
|
|
385
|
+
const line = readFileSync(file, "utf8").split(/\r?\n/)[0];
|
|
386
|
+
const port = Number(line);
|
|
387
|
+
if (port > 0) {
|
|
388
|
+
resolve(port);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (Date.now() > deadline) {
|
|
393
|
+
reject(/* @__PURE__ */ new Error("chrome-headless-shell did not open a DevTools port"));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
setTimeout(tick, 50);
|
|
397
|
+
};
|
|
398
|
+
tick();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function pickPageDebuggerUrl(targets) {
|
|
402
|
+
return targets.find((t) => t.webSocketDebuggerUrl && t.type !== "browser")?.webSocketDebuggerUrl;
|
|
403
|
+
}
|
|
404
|
+
async function pageDebuggerUrl(port, timeoutMs) {
|
|
405
|
+
const deadline = Date.now() + timeoutMs;
|
|
406
|
+
const listPages = async () => {
|
|
407
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
408
|
+
if (!res.ok) return void 0;
|
|
409
|
+
return pickPageDebuggerUrl(await res.json());
|
|
410
|
+
};
|
|
411
|
+
for (;;) {
|
|
412
|
+
const url = await listPages();
|
|
413
|
+
if (url) return url;
|
|
414
|
+
if (Date.now() > deadline) break;
|
|
415
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
416
|
+
}
|
|
417
|
+
const version = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
|
|
418
|
+
if (!version.webSocketDebuggerUrl) throw new Error("chrome-headless-shell reported no DevTools URL");
|
|
419
|
+
const browser = await cdpConnect(version.webSocketDebuggerUrl, timeoutMs);
|
|
420
|
+
try {
|
|
421
|
+
await browser.send("Target.createTarget", { url: "about:blank" });
|
|
422
|
+
} finally {
|
|
423
|
+
browser.close();
|
|
424
|
+
}
|
|
425
|
+
const created = await listPages();
|
|
426
|
+
if (!created) throw new Error("chrome-headless-shell reported no page target");
|
|
427
|
+
return created;
|
|
428
|
+
}
|
|
429
|
+
async function cdpConnect(url, timeoutMs) {
|
|
430
|
+
const ws = new WebSocket(url);
|
|
431
|
+
await new Promise((resolve, reject) => {
|
|
432
|
+
const t = setTimeout(() => reject(/* @__PURE__ */ new Error("CDP websocket timed out")), timeoutMs);
|
|
433
|
+
ws.addEventListener("open", () => {
|
|
434
|
+
clearTimeout(t);
|
|
435
|
+
resolve();
|
|
436
|
+
});
|
|
437
|
+
ws.addEventListener("error", () => {
|
|
438
|
+
clearTimeout(t);
|
|
439
|
+
reject(/* @__PURE__ */ new Error("CDP websocket failed"));
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
return new Cdp(ws);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Drops a chrome-headless-shell profile. Chrome can still write after SIGKILL,
|
|
446
|
+
* so `ENOTEMPTY` / `EBUSY` must not replace a PNG already in hand.
|
|
447
|
+
*/
|
|
448
|
+
function removeChromeProfile(dir, rm = rmSync) {
|
|
449
|
+
try {
|
|
450
|
+
rm(dir, {
|
|
451
|
+
recursive: true,
|
|
452
|
+
force: true,
|
|
453
|
+
maxRetries: 5,
|
|
454
|
+
retryDelay: 20
|
|
455
|
+
});
|
|
456
|
+
} catch {
|
|
457
|
+
scheduleChromeProfileSweep(dir, rm);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function scheduleChromeProfileSweep(dir, rm) {
|
|
461
|
+
setTimeout(() => {
|
|
462
|
+
try {
|
|
463
|
+
rm(dir, {
|
|
464
|
+
recursive: true,
|
|
465
|
+
force: true,
|
|
466
|
+
maxRetries: 5,
|
|
467
|
+
retryDelay: 50
|
|
468
|
+
});
|
|
469
|
+
} catch {}
|
|
470
|
+
}, 250).unref();
|
|
471
|
+
}
|
|
472
|
+
function waitForExit(child, timeoutMs) {
|
|
473
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
474
|
+
return new Promise((resolve) => {
|
|
475
|
+
const finish = () => {
|
|
476
|
+
clearTimeout(timer);
|
|
477
|
+
child.off("exit", finish);
|
|
478
|
+
resolve();
|
|
479
|
+
};
|
|
480
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
481
|
+
child.once("exit", finish);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/** Kills the shell and drops its profile. Never throws: the PNG is already in hand. */
|
|
485
|
+
async function disposeChrome(child, userData) {
|
|
486
|
+
try {
|
|
487
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
488
|
+
await waitForExit(child, 1e3);
|
|
489
|
+
} catch {}
|
|
490
|
+
removeChromeProfile(userData);
|
|
491
|
+
}
|
|
492
|
+
/** One Chrome process, one tab. `fn` must not keep the session after it returns. */
|
|
493
|
+
async function withChrome(bin, window, timeoutMs, fn) {
|
|
494
|
+
const userData = mkdtempSync(join(tmpdir(), `reeval-chrome-${process.pid}-`));
|
|
495
|
+
const child = spawn(bin, [
|
|
496
|
+
"--headless",
|
|
497
|
+
"--disable-gpu",
|
|
498
|
+
"--no-sandbox",
|
|
499
|
+
"--disable-dev-shm-usage",
|
|
500
|
+
"--hide-scrollbars",
|
|
501
|
+
"--remote-allow-origins=*",
|
|
502
|
+
`--window-size=${window.width},${window.height}`,
|
|
503
|
+
`--user-data-dir=${userData}`,
|
|
504
|
+
"--remote-debugging-port=0",
|
|
505
|
+
"about:blank"
|
|
506
|
+
], { stdio: [
|
|
507
|
+
"ignore",
|
|
508
|
+
"ignore",
|
|
509
|
+
"pipe"
|
|
510
|
+
] });
|
|
511
|
+
try {
|
|
512
|
+
const cdp = await cdpConnect(await pageDebuggerUrl(await waitForPortFile(join(userData, "DevToolsActivePort"), timeoutMs), timeoutMs), timeoutMs);
|
|
513
|
+
try {
|
|
514
|
+
await cdp.send("Page.enable");
|
|
515
|
+
await cdp.send("Page.setLifecycleEventsEnabled", { enabled: true });
|
|
516
|
+
await cdp.send("Emulation.setDeviceMetricsOverride", {
|
|
517
|
+
width: window.width,
|
|
518
|
+
height: window.height,
|
|
519
|
+
deviceScaleFactor: 1,
|
|
520
|
+
mobile: false
|
|
521
|
+
});
|
|
522
|
+
let scrollY = 0;
|
|
523
|
+
return await fn({
|
|
524
|
+
cdp,
|
|
525
|
+
window,
|
|
526
|
+
async navigate(url) {
|
|
527
|
+
scrollY = 0;
|
|
528
|
+
const nav = await cdp.send("Page.navigate", { url });
|
|
529
|
+
if (nav?.errorText) throw new Error(`c.capture() navigation failed: ${nav.errorText}`);
|
|
530
|
+
return nav;
|
|
531
|
+
},
|
|
532
|
+
wait(wait, waitMs, nav) {
|
|
533
|
+
return waitForPage(cdp, wait, waitMs, nav);
|
|
534
|
+
},
|
|
535
|
+
async screenshot(fullPage) {
|
|
536
|
+
const shot = await cdp.send("Page.captureScreenshot", {
|
|
537
|
+
format: "png",
|
|
538
|
+
captureBeyondViewport: fullPage
|
|
539
|
+
});
|
|
540
|
+
if (!shot.data) throw new Error("Page.captureScreenshot returned no data");
|
|
541
|
+
return Buffer.from(shot.data, "base64");
|
|
542
|
+
},
|
|
543
|
+
async dispatch(action, page) {
|
|
544
|
+
scrollY = await dispatchInput(cdp, action, window, page, scrollY);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
} finally {
|
|
548
|
+
cdp.close();
|
|
549
|
+
}
|
|
550
|
+
} finally {
|
|
551
|
+
await disposeChrome(child, userData);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/** Arm the wait, then navigate, then wait. `arm` is before `Page.navigate`. */
|
|
555
|
+
async function navigateAndWait(session, url, wait, timeoutMs) {
|
|
556
|
+
if (isMs(wait)) {
|
|
557
|
+
const nav = await session.navigate(url);
|
|
558
|
+
await session.wait(wait, timeoutMs, nav);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const page = listenForCaptureWait(session.cdp, wait);
|
|
562
|
+
page.arm();
|
|
563
|
+
const nav = await session.navigate(url);
|
|
564
|
+
await page.ready(nav, timeoutMs);
|
|
565
|
+
}
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/capture/poll.ts
|
|
568
|
+
/** Any HTTP response, including 404, means something accepted the connection. */
|
|
569
|
+
async function pollHttp(url, timeoutMs, signal) {
|
|
570
|
+
const deadline = Date.now() + timeoutMs;
|
|
571
|
+
let last;
|
|
572
|
+
while (Date.now() < deadline) {
|
|
573
|
+
signal?.throwIfAborted();
|
|
574
|
+
const left = Math.max(1, deadline - Date.now());
|
|
575
|
+
try {
|
|
576
|
+
await fetch(url, { signal: AbortSignal.any([AbortSignal.timeout(Math.min(1e3, left)), ...signal ? [signal] : []]) });
|
|
577
|
+
return;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
last = error;
|
|
580
|
+
if (signal?.aborted) throw error;
|
|
581
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
throw last instanceof Error ? last : /* @__PURE__ */ new Error("poll timed out");
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/capture/stub.ts
|
|
588
|
+
const PNG_SIG = Buffer.from([
|
|
589
|
+
137,
|
|
590
|
+
80,
|
|
591
|
+
78,
|
|
592
|
+
71,
|
|
593
|
+
13,
|
|
594
|
+
10,
|
|
595
|
+
26,
|
|
596
|
+
10
|
|
597
|
+
]);
|
|
598
|
+
function chunk(type, data) {
|
|
599
|
+
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
600
|
+
const out = Buffer.alloc(12 + data.length);
|
|
601
|
+
out.writeUInt32BE(data.length, 0);
|
|
602
|
+
body.copy(out, 4);
|
|
603
|
+
out.writeUInt32BE(crc32(body), 8 + data.length);
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
/** A valid 1×1 PNG whose bytes are unique for `(name, port, path, viewport)`. */
|
|
607
|
+
function stubPng(name, port, path, viewport) {
|
|
608
|
+
const digest = createHash("sha256").update(`${name}\0${port}\0${path}\0${viewport === "full-page" ? "full-page" : `${viewport.width}x${viewport.height}`}`).digest();
|
|
609
|
+
const ihdr = Buffer.alloc(13);
|
|
610
|
+
ihdr.writeUInt32BE(1, 0);
|
|
611
|
+
ihdr.writeUInt32BE(1, 4);
|
|
612
|
+
ihdr[8] = 8;
|
|
613
|
+
ihdr[9] = 2;
|
|
614
|
+
const scanline = Buffer.from([
|
|
615
|
+
0,
|
|
616
|
+
digest[0] ?? 0,
|
|
617
|
+
digest[1] ?? 0,
|
|
618
|
+
digest[2] ?? 0
|
|
619
|
+
]);
|
|
620
|
+
const idat = deflateSync(scanline);
|
|
621
|
+
const text = Buffer.concat([Buffer.from("Comment\0", "ascii"), Buffer.from(digest.toString("hex"), "ascii")]);
|
|
622
|
+
return Buffer.concat([
|
|
623
|
+
PNG_SIG,
|
|
624
|
+
chunk("IHDR", ihdr),
|
|
625
|
+
chunk("tEXt", text),
|
|
626
|
+
chunk("IDAT", idat),
|
|
627
|
+
chunk("IEND", Buffer.alloc(0))
|
|
628
|
+
]);
|
|
629
|
+
}
|
|
630
|
+
/** PNG IHDR width and height; throws if the bytes are not a PNG. */
|
|
631
|
+
function pngSize(bytes) {
|
|
632
|
+
if (bytes.length < 24 || Buffer.from(bytes.subarray(0, 8)).compare(PNG_SIG) !== 0) throw new Error("not a PNG");
|
|
633
|
+
const view = Buffer.from(bytes.subarray(16, 24));
|
|
634
|
+
return {
|
|
635
|
+
width: view.readUInt32BE(0),
|
|
636
|
+
height: view.readUInt32BE(4)
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/capture/index.ts
|
|
641
|
+
/** Default CSS pixels when the call and cascade omit `viewport`. */
|
|
642
|
+
const DEFAULT_VIEWPORT = {
|
|
643
|
+
width: 1280,
|
|
644
|
+
height: 720
|
|
645
|
+
};
|
|
646
|
+
const CAPTURE_NAME = /^[a-z0-9_-]+$/;
|
|
647
|
+
const CAPTURE_WAITS = ["load", "idle"];
|
|
648
|
+
/** `'load'` when the call omits `wait`. `'load'`, `'idle'` or `{ ms }`. */
|
|
649
|
+
function resolveWait(wait) {
|
|
650
|
+
if (wait === void 0) return "load";
|
|
651
|
+
if (CAPTURE_WAITS.includes(wait)) return wait;
|
|
652
|
+
if (typeof wait === "object" && wait !== null && Number.isFinite(wait.ms) && wait.ms >= 0 && Object.keys(wait).every((k) => k === "ms")) return { ms: wait.ms };
|
|
653
|
+
throw new Error(`c.capture() wait is 'load', 'idle' or { ms } (got ${JSON.stringify(wait)})`);
|
|
654
|
+
}
|
|
655
|
+
function parseInstructions(value) {
|
|
656
|
+
if (value === void 0) return [];
|
|
657
|
+
if (!Array.isArray(value)) throw new Error(`c.capture() instructions is an array of strings and { wait } (got ${JSON.stringify(value)})`);
|
|
658
|
+
return value.map((step, i) => {
|
|
659
|
+
if (typeof step === "string") {
|
|
660
|
+
if (!step.trim()) throw new Error(`c.capture() instructions[${i}] is an empty string`);
|
|
661
|
+
return step;
|
|
662
|
+
}
|
|
663
|
+
if (typeof step === "object" && step !== null && "wait" in step) return { wait: resolveWait(step.wait) };
|
|
664
|
+
throw new Error(`c.capture() instructions[${i}] is a string or { wait } (got ${JSON.stringify(step)})`);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
function instructionGoals(steps) {
|
|
668
|
+
return steps.filter((s) => typeof s === "string");
|
|
669
|
+
}
|
|
670
|
+
/** Live window and whether the saved PNG uses `captureBeyondViewport`. */
|
|
671
|
+
function resolveCaptureViewport(viewport) {
|
|
672
|
+
const asked = viewport ?? DEFAULT_VIEWPORT;
|
|
673
|
+
if (asked === "full-page") return {
|
|
674
|
+
viewport: "full-page",
|
|
675
|
+
window: DEFAULT_VIEWPORT,
|
|
676
|
+
fullPage: true
|
|
677
|
+
};
|
|
678
|
+
return {
|
|
679
|
+
viewport: asked,
|
|
680
|
+
window: asked,
|
|
681
|
+
fullPage: false
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/** `~/.cache/reeval` when that's the resolved dir; otherwise the absolute path. */
|
|
685
|
+
function cacheLabel(dir = cacheDir()) {
|
|
686
|
+
const home = homedir();
|
|
687
|
+
if (dir.replaceAll("\\", "/") === join(home, ".cache", "reeval").replaceAll("\\", "/")) return "~/.cache/reeval";
|
|
688
|
+
return dir;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Env is named first when both refuse. `undefined` means the host binary is allowed.
|
|
692
|
+
*/
|
|
693
|
+
function refusedBrowser(capture) {
|
|
694
|
+
if (process.env.REEVAL_CAPTURE_BROWSER === "0") return "c.capture() is refused: REEVAL_CAPTURE_BROWSER=0";
|
|
695
|
+
if (capture.browser === false) return "c.capture() is refused: capture.browser is false";
|
|
696
|
+
}
|
|
697
|
+
function resolveCaptureTarget(target) {
|
|
698
|
+
if (typeof target === "number") {
|
|
699
|
+
if (!(Number.isInteger(target) && target >= 1 && target <= 65535)) throw new Error(`c.capture() port is a container port from 1 to 65535 (got ${String(target)})`);
|
|
700
|
+
return target;
|
|
701
|
+
}
|
|
702
|
+
if (target !== null && typeof target === "object" && typeof target.port === "number" && typeof target.kill === "function") return resolveCaptureTarget(target.port);
|
|
703
|
+
throw new Error("c.capture() needs a port or a ServeHandle as the first argument");
|
|
704
|
+
}
|
|
705
|
+
/** Artifact name from a path: `/login` → `login`, `/foo/bar` → `foo-bar`, `/` → `root`. */
|
|
706
|
+
function nameFromPath(path) {
|
|
707
|
+
return (path.split(/[?#]/, 1)[0] ?? "").replaceAll("\\", "/").split("/").filter(Boolean).join("-").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "root";
|
|
708
|
+
}
|
|
709
|
+
function resolveCaptureCall(target, call, cascade) {
|
|
710
|
+
if (call && Object.hasOwn(call, "port")) throw new Error("c.capture() takes the port as the first argument, not in the options");
|
|
711
|
+
if (call && Object.hasOwn(call, "use")) throw new Error("c.capture() has no use. Set model, or pass fakeInstructions from reeval/testing as instructions.");
|
|
712
|
+
if (call && Object.hasOwn(call, "fake")) throw new Error("c.capture() has no fake. Tests pass fakeInstructions(...) as instructions.");
|
|
713
|
+
if (call?.model !== void 0) checkCallModel(call.model);
|
|
714
|
+
const port = resolveCaptureTarget(target);
|
|
715
|
+
const path = call?.path || "/";
|
|
716
|
+
const name = call?.name ?? nameFromPath(path);
|
|
717
|
+
checkCaptureName(name);
|
|
718
|
+
const asked = resolveCaptureViewport(call?.viewport ?? cascade.viewport);
|
|
719
|
+
return {
|
|
720
|
+
port,
|
|
721
|
+
name,
|
|
722
|
+
path,
|
|
723
|
+
viewport: asked.viewport,
|
|
724
|
+
window: asked.window,
|
|
725
|
+
fullPage: asked.fullPage,
|
|
726
|
+
timeoutMs: parseDuration(call?.timeout ?? cascade.timeout ?? "60s"),
|
|
727
|
+
wait: resolveWait(call?.wait),
|
|
728
|
+
instructions: parseInstructions(call?.instructions),
|
|
729
|
+
model: call?.model ?? cascade.model,
|
|
730
|
+
fake: fakeScriptFrom(call?.instructions)
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function fakeScriptFrom(value) {
|
|
734
|
+
const steps = fakeInstructionSteps(value);
|
|
735
|
+
if (!steps) return void 0;
|
|
736
|
+
const script = [];
|
|
737
|
+
for (const step of steps) {
|
|
738
|
+
if (isWaitStep(step)) continue;
|
|
739
|
+
script.push(step);
|
|
740
|
+
}
|
|
741
|
+
return script;
|
|
742
|
+
}
|
|
743
|
+
function checkCallModel(model) {
|
|
744
|
+
if (!model.includes("/")) throw new Error(`c.capture() model is a 'provider/model' string, e.g. 'google/gemini-3.8-flash' (got ${JSON.stringify(model)})`);
|
|
745
|
+
if (model.startsWith("typesafe-ai/")) throw new Error(`c.capture() model "${model}" is an evaluation model: it scores yes/no questions and can't drive a capture. Use a language model that accepts images, e.g. 'google/gemini-3.8-flash'.`);
|
|
746
|
+
}
|
|
747
|
+
function checkCaptureName(name) {
|
|
748
|
+
if (!CAPTURE_NAME.test(name)) throw new Error(`c.capture() name must match [a-z0-9_-]+ (got ${JSON.stringify(name)})`);
|
|
749
|
+
}
|
|
750
|
+
/** A free host port. Closed before the promise resolves, so the caller can bind it. */
|
|
751
|
+
function allocatePort() {
|
|
752
|
+
return new Promise((resolve, reject) => {
|
|
753
|
+
const server = createServer();
|
|
754
|
+
server.once("error", reject);
|
|
755
|
+
server.listen(0, "127.0.0.1", () => {
|
|
756
|
+
const addr = server.address();
|
|
757
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
758
|
+
server.close((error) => error ? reject(error) : resolve(port));
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/** Origin plus a path (`/login`), without a doubled slash. */
|
|
763
|
+
function pageUrl(origin, path) {
|
|
764
|
+
const base = origin.endsWith("/") ? origin : `${origin}/`;
|
|
765
|
+
const rel = path.startsWith("/") ? path.slice(1) : path;
|
|
766
|
+
return new URL(rel, base).href;
|
|
767
|
+
}
|
|
768
|
+
/** Internal settle between moves; not an authored wait. */
|
|
769
|
+
const SETTLE_MS = 80;
|
|
770
|
+
function remainingOrThrow(remaining, detail) {
|
|
771
|
+
const left = remaining();
|
|
772
|
+
if (left <= 0) throw new Error(`c.capture() timed out on ${detail}`);
|
|
773
|
+
return left;
|
|
774
|
+
}
|
|
775
|
+
async function settle() {
|
|
776
|
+
await new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
|
|
777
|
+
}
|
|
778
|
+
function asUsage(value) {
|
|
779
|
+
return {
|
|
780
|
+
input: typeof value.inputTokens === "number" ? value.inputTokens : value.inputTokens?.total ?? 0,
|
|
781
|
+
output: typeof value.outputTokens === "number" ? value.outputTokens : value.outputTokens?.total ?? 0,
|
|
782
|
+
cacheRead: 0,
|
|
783
|
+
cacheWrite: 0,
|
|
784
|
+
reasoning: null
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function actionFromTool(name, input) {
|
|
788
|
+
if (name === "done") return { type: "done" };
|
|
789
|
+
if (name === "type") return {
|
|
790
|
+
type: "type",
|
|
791
|
+
text: String(input.text ?? "")
|
|
792
|
+
};
|
|
793
|
+
if (name === "key") return {
|
|
794
|
+
type: "key",
|
|
795
|
+
key: input.key === "Tab" ? "Tab" : "Enter"
|
|
796
|
+
};
|
|
797
|
+
if (name === "scroll") return {
|
|
798
|
+
type: "scroll",
|
|
799
|
+
x: Number(input.x),
|
|
800
|
+
y: Number(input.y),
|
|
801
|
+
dy: Number(input.dy)
|
|
802
|
+
};
|
|
803
|
+
if (name === "drag") return {
|
|
804
|
+
type: "drag",
|
|
805
|
+
x: Number(input.x),
|
|
806
|
+
y: Number(input.y),
|
|
807
|
+
toX: Number(input.toX),
|
|
808
|
+
toY: Number(input.toY)
|
|
809
|
+
};
|
|
810
|
+
if (name === "hover") return {
|
|
811
|
+
type: "hover",
|
|
812
|
+
x: Number(input.x),
|
|
813
|
+
y: Number(input.y)
|
|
814
|
+
};
|
|
815
|
+
return {
|
|
816
|
+
type: "click",
|
|
817
|
+
x: Number(input.x),
|
|
818
|
+
y: Number(input.y)
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const useTools = {
|
|
822
|
+
click: tool({
|
|
823
|
+
description: "Click at CSS pixels on the screenshot.",
|
|
824
|
+
inputSchema: z.object({
|
|
825
|
+
x: z.number(),
|
|
826
|
+
y: z.number()
|
|
827
|
+
})
|
|
828
|
+
}),
|
|
829
|
+
type: tool({
|
|
830
|
+
description: "Insert text into the focused field. Click the field first.",
|
|
831
|
+
inputSchema: z.object({ text: z.string() })
|
|
832
|
+
}),
|
|
833
|
+
key: tool({
|
|
834
|
+
description: "Press Tab or Enter.",
|
|
835
|
+
inputSchema: z.object({ key: z.enum(["Tab", "Enter"]) })
|
|
836
|
+
}),
|
|
837
|
+
scroll: tool({
|
|
838
|
+
description: "Wheel-scroll a constrained scroller at (x, y).",
|
|
839
|
+
inputSchema: z.object({
|
|
840
|
+
x: z.number(),
|
|
841
|
+
y: z.number(),
|
|
842
|
+
dy: z.number()
|
|
843
|
+
})
|
|
844
|
+
}),
|
|
845
|
+
drag: tool({
|
|
846
|
+
description: "Drag from (x, y) to (toX, toY).",
|
|
847
|
+
inputSchema: z.object({
|
|
848
|
+
x: z.number(),
|
|
849
|
+
y: z.number(),
|
|
850
|
+
toX: z.number(),
|
|
851
|
+
toY: z.number()
|
|
852
|
+
})
|
|
853
|
+
}),
|
|
854
|
+
hover: tool({
|
|
855
|
+
description: "Move the pointer to (x, y).",
|
|
856
|
+
inputSchema: z.object({
|
|
857
|
+
x: z.number(),
|
|
858
|
+
y: z.number()
|
|
859
|
+
})
|
|
860
|
+
}),
|
|
861
|
+
done: tool({
|
|
862
|
+
description: "The goal is finished. Leave the loop.",
|
|
863
|
+
inputSchema: z.object({})
|
|
864
|
+
})
|
|
865
|
+
};
|
|
866
|
+
async function runFakeScript(session, script, goal, remaining) {
|
|
867
|
+
let moves = 0;
|
|
868
|
+
for (const action of script) {
|
|
869
|
+
remainingOrThrow(remaining, JSON.stringify(goal));
|
|
870
|
+
if (action.type === "done") return moves;
|
|
871
|
+
if (moves >= 32) throw new Error(`c.capture() instruction ${JSON.stringify(goal)} did not finish in 32 moves`);
|
|
872
|
+
const page = pngSize(await session.screenshot(true));
|
|
873
|
+
debugTrace("check", "capture-use", {
|
|
874
|
+
goal,
|
|
875
|
+
action,
|
|
876
|
+
page
|
|
877
|
+
}, `capture use ${action.type}`);
|
|
878
|
+
await session.dispatch(action, page);
|
|
879
|
+
moves++;
|
|
880
|
+
await settle();
|
|
881
|
+
}
|
|
882
|
+
while (remaining() > 0) await settle();
|
|
883
|
+
throw new Error(`c.capture() timed out on ${JSON.stringify(goal)}`);
|
|
884
|
+
}
|
|
885
|
+
async function runModelGoal(session, goal, modelName, remaining, loadModel) {
|
|
886
|
+
const model = await loadModel(modelName);
|
|
887
|
+
let moves = 0;
|
|
888
|
+
let usage = null;
|
|
889
|
+
while (moves < 32) {
|
|
890
|
+
remainingOrThrow(remaining, JSON.stringify(goal));
|
|
891
|
+
const ephemeral = await session.screenshot(true);
|
|
892
|
+
const page = pngSize(ephemeral);
|
|
893
|
+
debugTrace("check", "capture-shot", {
|
|
894
|
+
goal,
|
|
895
|
+
bytes: ephemeral.length,
|
|
896
|
+
page
|
|
897
|
+
}, false);
|
|
898
|
+
let chosen;
|
|
899
|
+
const result = await generateText({
|
|
900
|
+
model,
|
|
901
|
+
system: "You drive a web page from screenshots. Call one tool. Coordinates are CSS pixels on the full-page image. Call done when the goal is met. Do not mention CDP.",
|
|
902
|
+
messages: [{
|
|
903
|
+
role: "user",
|
|
904
|
+
content: [{
|
|
905
|
+
type: "text",
|
|
906
|
+
text: goal
|
|
907
|
+
}, {
|
|
908
|
+
type: "image",
|
|
909
|
+
image: ephemeral
|
|
910
|
+
}]
|
|
911
|
+
}],
|
|
912
|
+
tools: useTools,
|
|
913
|
+
toolChoice: "required",
|
|
914
|
+
stopWhen: stepCountIs(1),
|
|
915
|
+
abortSignal: AbortSignal.timeout(remainingOrThrow(remaining, JSON.stringify(goal)))
|
|
916
|
+
});
|
|
917
|
+
usage = addUsage(usage, asUsage(result.usage ?? {}));
|
|
918
|
+
const call = result.toolCalls?.[0];
|
|
919
|
+
if (call) chosen = actionFromTool(call.toolName, call.input ?? {});
|
|
920
|
+
if (!chosen) throw new Error(`c.capture() instruction ${JSON.stringify(goal)} returned no action`);
|
|
921
|
+
debugLog("check", `capture use ${chosen.type} on ${JSON.stringify(goal)}`);
|
|
922
|
+
debugTrace("check", "capture-use", {
|
|
923
|
+
goal,
|
|
924
|
+
action: chosen,
|
|
925
|
+
page
|
|
926
|
+
}, `capture use ${chosen.type}`);
|
|
927
|
+
if (chosen.type === "done") return {
|
|
928
|
+
moves,
|
|
929
|
+
usage
|
|
930
|
+
};
|
|
931
|
+
await session.dispatch(chosen, page);
|
|
932
|
+
moves++;
|
|
933
|
+
await settle();
|
|
934
|
+
}
|
|
935
|
+
throw new Error(`c.capture() instruction ${JSON.stringify(goal)} did not finish in 32 moves`);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Top-level wait, then the instruction loop, then the saved PNG.
|
|
939
|
+
* Throws before `screenshot` when the budget or move cap is hit.
|
|
940
|
+
*/
|
|
941
|
+
async function runJourney(session, url, options) {
|
|
942
|
+
const { wait, timeoutMs, fullPage, instructions, use, remaining } = options;
|
|
943
|
+
remainingOrThrow(remaining, "navigate");
|
|
944
|
+
await navigateAndWait(session, url, wait, Math.min(timeoutMs, remaining()));
|
|
945
|
+
let moves = 0;
|
|
946
|
+
let usage = null;
|
|
947
|
+
let stringIndex = 0;
|
|
948
|
+
for (const step of instructions) {
|
|
949
|
+
if (typeof step !== "string") {
|
|
950
|
+
remainingOrThrow(remaining, "wait");
|
|
951
|
+
await session.wait(step.wait, remaining(), void 0);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
remainingOrThrow(remaining, JSON.stringify(step));
|
|
955
|
+
debugLog("check", `capture instruction ${JSON.stringify(step)}`);
|
|
956
|
+
if (use && "fake" in use) {
|
|
957
|
+
const script = use.fake[stringIndex] ?? [];
|
|
958
|
+
moves += await runFakeScript(session, script, step, remaining);
|
|
959
|
+
} else if (use && "model" in use) {
|
|
960
|
+
if (!options.loadModel) throw new Error("c.capture() instructions need a model loader");
|
|
961
|
+
const ran = await runModelGoal(session, step, use.model, remaining, options.loadModel);
|
|
962
|
+
moves += ran.moves;
|
|
963
|
+
usage = addUsage(usage, ran.usage);
|
|
964
|
+
}
|
|
965
|
+
stringIndex++;
|
|
966
|
+
await settle();
|
|
967
|
+
}
|
|
968
|
+
remainingOrThrow(remaining, "screenshot");
|
|
969
|
+
return {
|
|
970
|
+
png: await session.screenshot(fullPage),
|
|
971
|
+
moves,
|
|
972
|
+
model: use && "model" in use ? use.model : use && "fake" in use ? "fake" : void 0,
|
|
973
|
+
usage
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function captureUseSpend(result) {
|
|
977
|
+
if (!result.model) return void 0;
|
|
978
|
+
return {
|
|
979
|
+
model: result.model,
|
|
980
|
+
moves: result.moves,
|
|
981
|
+
usage: result.usage
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
//#endregion
|
|
985
|
+
//#region src/diff.ts
|
|
986
|
+
const GIT = "git -c user.name=reeval -c user.email=reeval@localhost -c core.hooksPath=/dev/null";
|
|
987
|
+
const SNAPSHOT_ENV = "export GIT_DIR=\"$PWD/../.reeval-git-$(basename \"$PWD\")\" GIT_WORK_TREE=\"$PWD\"";
|
|
988
|
+
/** Records the workspace state before the agent runs, in a git dir outside the workspace. */
|
|
989
|
+
async function snapshot(sandbox, signal) {
|
|
990
|
+
const r = await sandbox.exec(`${SNAPSHOT_ENV} && ${GIT} init -q && ${GIT} add -A && ${GIT} commit -q --allow-empty -m base`, { signal });
|
|
991
|
+
if (r.exitCode !== 0) throw new InfraError(`Failed to snapshot the workspace: ${r.stderr.trim()}`);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Moves the snapshot forward for one workspace file as it is now, so later diffs show only
|
|
995
|
+
* changes made to it after this. Only that path is committed: the other changes stay in the
|
|
996
|
+
* diff. `-f` tracks a file the workspace's `.gitignore` covers, so a later edit to it shows too.
|
|
997
|
+
*/
|
|
998
|
+
async function acceptIntoSnapshot(sandbox, path, signal) {
|
|
999
|
+
const quoted = shellQuote(path);
|
|
1000
|
+
const r = await sandbox.exec(`${SNAPSHOT_ENV} && export GIT_LITERAL_PATHSPECS=1 && ${GIT} add -f -- ${quoted} && ${GIT} commit -q --allow-empty --no-verify -m ${shellQuote(`place ${path}`)} --only -- ${quoted}`, { signal });
|
|
1001
|
+
if (r.exitCode !== 0) throw new InfraError(`Failed to keep ${path} out of the diff: ${r.stderr.trim()}`);
|
|
1002
|
+
}
|
|
1003
|
+
async function diffSinceSnapshot(sandbox, signal) {
|
|
1004
|
+
const env = `${SNAPSHOT_ENV} && ${GIT} add -A && `;
|
|
1005
|
+
const patch = await sandbox.exec(`${env}${GIT} diff --cached --binary HEAD`, { signal });
|
|
1006
|
+
const names = await sandbox.exec(`${env}${GIT} diff --cached --name-status --no-renames HEAD`, { signal });
|
|
1007
|
+
if (patch.exitCode !== 0 || names.exitCode !== 0) throw new InfraError(`Failed to diff the workspace: ${patch.stderr || names.stderr}`);
|
|
1008
|
+
const status = {
|
|
1009
|
+
A: "added",
|
|
1010
|
+
M: "modified",
|
|
1011
|
+
D: "deleted"
|
|
1012
|
+
};
|
|
1013
|
+
const files = names.stdout.split("\n").filter(Boolean).map((line) => {
|
|
1014
|
+
const [code = "M", ...rest] = line.split(" ");
|
|
1015
|
+
return {
|
|
1016
|
+
path: rest.join(" "),
|
|
1017
|
+
status: status[code[0]] ?? "modified"
|
|
1018
|
+
};
|
|
1019
|
+
});
|
|
1020
|
+
return {
|
|
1021
|
+
patch: patch.stdout,
|
|
1022
|
+
files
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/attempt/checks.ts
|
|
1027
|
+
/** Runs a check or setup step with the checks' brokered secrets swapped in (see brokerPhases). */
|
|
1028
|
+
const asCheck = (a, fn) => a.phases ? a.phases.checks(fn) : fn();
|
|
1029
|
+
function requireSandbox(a, who = "c.exec() or c.readFile()") {
|
|
1030
|
+
if (!a.sandbox) throw new Error(`No sandbox yet: call c.send() or await c.sandbox() before ${who}`);
|
|
1031
|
+
return a.sandbox;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* `c.exec` runs with the checks' variables and secrets, each run recorded and
|
|
1035
|
+
* timed; `c.readFile` reads the sandbox as is; `c.writeFile` places a file the diff leaves out.
|
|
1036
|
+
*/
|
|
1037
|
+
function createChecks(a) {
|
|
1038
|
+
const checkEnv = () => a.env?.checkVars ?? {};
|
|
1039
|
+
const execForeground = async (command) => {
|
|
1040
|
+
const box = requireSandbox(a);
|
|
1041
|
+
const start = performance.now();
|
|
1042
|
+
a.report({
|
|
1043
|
+
kind: "check-start",
|
|
1044
|
+
command
|
|
1045
|
+
});
|
|
1046
|
+
const r = await a.timed("checksMs", () => asCheck(a, () => box.exec(command, {
|
|
1047
|
+
signal: a.signal,
|
|
1048
|
+
env: checkEnv()
|
|
1049
|
+
})));
|
|
1050
|
+
const durationMs = Math.round(performance.now() - start);
|
|
1051
|
+
a.record.checks.push({
|
|
1052
|
+
command,
|
|
1053
|
+
...r,
|
|
1054
|
+
durationMs
|
|
1055
|
+
});
|
|
1056
|
+
debugLog("check", `$ ${clip(command, 160)} → exit ${r.exitCode} · ${formatDuration(durationMs)}`);
|
|
1057
|
+
debugTrace("check", "output", {
|
|
1058
|
+
command,
|
|
1059
|
+
...r,
|
|
1060
|
+
durationMs
|
|
1061
|
+
}, `check output: ${clip(command, 80)}`);
|
|
1062
|
+
a.report({
|
|
1063
|
+
kind: "check-end",
|
|
1064
|
+
command,
|
|
1065
|
+
exitCode: r.exitCode,
|
|
1066
|
+
ms: durationMs
|
|
1067
|
+
});
|
|
1068
|
+
return r;
|
|
1069
|
+
};
|
|
1070
|
+
function exec(command, options) {
|
|
1071
|
+
if (options?.background) return import("./capture-DS-cPyuE.js").then(({ startBackground }) => startBackground(a, command));
|
|
1072
|
+
return Object.defineProperty(execForeground(command), SUBJECT, { value: command });
|
|
1073
|
+
}
|
|
1074
|
+
const readFile = (path) => requireSandbox(a).readFile(path);
|
|
1075
|
+
const writeFile = async (path, contents) => {
|
|
1076
|
+
const box = requireSandbox(a, "c.writeFile()");
|
|
1077
|
+
const home = path.startsWith("~/");
|
|
1078
|
+
const normal = posix.normalize((home ? path.slice(2) : path).replaceAll("\\", "/"));
|
|
1079
|
+
if (normal === "." || /^([a-zA-Z]:)?\//.test(normal) || /^\.\.(\/|$)/.test(normal)) throw new Error(`c.writeFile() takes a workspace-relative path or ~/…, not ${JSON.stringify(path)}`);
|
|
1080
|
+
a.signal.throwIfAborted();
|
|
1081
|
+
await box.writeFile(home ? `~/${normal}` : normal, contents);
|
|
1082
|
+
if (!home) await acceptIntoSnapshot(box, normal, a.signal);
|
|
1083
|
+
const bytes = typeof contents === "string" ? Buffer.byteLength(contents) : contents.byteLength;
|
|
1084
|
+
debugLog("check", `wrote ${path} (${bytes} bytes)${home ? "" : ", kept out of the diff until something changes it"}`);
|
|
1085
|
+
};
|
|
1086
|
+
return {
|
|
1087
|
+
exec,
|
|
1088
|
+
readFile,
|
|
1089
|
+
writeFile
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/attempt/capture.ts
|
|
1094
|
+
const BG_LOG = "/tmp/reeval-bg.log";
|
|
1095
|
+
const isEvaluationModel = (model) => model.startsWith("typesafe-ai/");
|
|
1096
|
+
function defaultCaptureModel() {
|
|
1097
|
+
try {
|
|
1098
|
+
const { config } = getRunContext();
|
|
1099
|
+
const analysis = config.analysis.model;
|
|
1100
|
+
if (typeof analysis === "string" && !isEvaluationModel(analysis)) return analysis;
|
|
1101
|
+
const judge = config.judge.model;
|
|
1102
|
+
if (typeof judge === "string" && !isEvaluationModel(judge)) return judge;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function resolveUse(resolved, fakeSandbox) {
|
|
1108
|
+
if (resolved.fake) return { fake: resolved.fake };
|
|
1109
|
+
if (resolved.model) return { model: resolved.model };
|
|
1110
|
+
if (!resolved.instructions.length) return void 0;
|
|
1111
|
+
const fallback = defaultCaptureModel();
|
|
1112
|
+
if (fallback) return { model: fallback };
|
|
1113
|
+
if (fakeSandbox) return void 0;
|
|
1114
|
+
throw new Error("c.capture() instructions need a vision model. Set capture.model, pass model on the call, or analysis.model to a language model that accepts images. Tests pass fakeInstructions from reeval/testing.");
|
|
1115
|
+
}
|
|
1116
|
+
/** Starts `command` without waiting for exit. Teardown kills it if the eval didn't. */
|
|
1117
|
+
async function startBackground(a, command, extraEnv = {}) {
|
|
1118
|
+
const box = requireSandbox(a);
|
|
1119
|
+
const wrapped = `${command} >${BG_LOG} 2>&1 & echo $!`;
|
|
1120
|
+
const r = await asCheck(a, () => box.exec(wrapped, {
|
|
1121
|
+
signal: a.signal,
|
|
1122
|
+
env: {
|
|
1123
|
+
...a.env?.checkVars ?? {},
|
|
1124
|
+
...extraEnv
|
|
1125
|
+
}
|
|
1126
|
+
}));
|
|
1127
|
+
const pid = r.stdout.trim().split(/\s+/).at(-1) ?? "";
|
|
1128
|
+
if (!/^\d+$/.test(pid)) throw new Error(`c.exec({ background: true }) did not start (no pid):\n${(r.stderr || r.stdout).trim()}`);
|
|
1129
|
+
let killed = false;
|
|
1130
|
+
const handle = { async kill() {
|
|
1131
|
+
if (killed) return;
|
|
1132
|
+
killed = true;
|
|
1133
|
+
const win = a.spec.sandbox.kind === "fake" && process.platform === "win32" ? `; wp=$(tr -d '\\r\\n' < /proc/${pid}/winpid 2>/dev/null) && taskkill /F /PID "$wp" >/dev/null 2>&1 || true` : "";
|
|
1134
|
+
await box.exec(`kill -9 ${pid} 2>/dev/null || true${win}`).catch(() => {});
|
|
1135
|
+
} };
|
|
1136
|
+
a.backgrounds.push(handle);
|
|
1137
|
+
return handle;
|
|
1138
|
+
}
|
|
1139
|
+
async function serveCommand(a, command, port) {
|
|
1140
|
+
const bound = port ?? await allocatePort();
|
|
1141
|
+
return {
|
|
1142
|
+
...await startBackground(a, command, { PORT: String(bound) }),
|
|
1143
|
+
port: bound
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
async function capturePage(a, target, call) {
|
|
1147
|
+
const refused = refusedBrowser(a.spec.capture);
|
|
1148
|
+
if (refused) throw new Error(refused);
|
|
1149
|
+
const resolved = resolveCaptureCall(target, call, a.spec.capture);
|
|
1150
|
+
if (a.captureNames.has(resolved.name)) throw new Error(`c.capture() reused the name "${resolved.name}"`);
|
|
1151
|
+
const box = requireSandbox(a, "c.capture()");
|
|
1152
|
+
if (!box.getPortEndpoint) throw new Error("c.capture() needs a sandbox that can tunnel a port");
|
|
1153
|
+
const { url: origin } = await box.getPortEndpoint({
|
|
1154
|
+
port: resolved.port,
|
|
1155
|
+
protocol: "http"
|
|
1156
|
+
});
|
|
1157
|
+
try {
|
|
1158
|
+
await pollHttp(pageUrl(origin, "/"), resolved.timeoutMs, a.signal);
|
|
1159
|
+
} catch {
|
|
1160
|
+
throw new Error(`nothing listening on port ${resolved.port}`);
|
|
1161
|
+
}
|
|
1162
|
+
const fake = a.spec.sandbox.kind === "fake";
|
|
1163
|
+
const use = resolveUse(resolved, fake);
|
|
1164
|
+
a.captureNames.add(resolved.name);
|
|
1165
|
+
const started = Date.now();
|
|
1166
|
+
const remaining = () => Math.max(0, resolved.timeoutMs - (Date.now() - started));
|
|
1167
|
+
let moves = 0;
|
|
1168
|
+
const png = fake ? stubPng(resolved.name, resolved.port, resolved.path, resolved.viewport) : await shot(a, pageUrl(origin, resolved.path), {
|
|
1169
|
+
...resolved,
|
|
1170
|
+
use
|
|
1171
|
+
}, remaining, (n) => {
|
|
1172
|
+
moves = n;
|
|
1173
|
+
});
|
|
1174
|
+
const digest = sha256(png);
|
|
1175
|
+
const file = {
|
|
1176
|
+
path: `artifacts/${resolved.name}.png`,
|
|
1177
|
+
sha256: digest,
|
|
1178
|
+
bytes: png.length
|
|
1179
|
+
};
|
|
1180
|
+
const goals = instructionGoals(resolved.instructions);
|
|
1181
|
+
const capture = {
|
|
1182
|
+
port: resolved.port,
|
|
1183
|
+
path: resolved.path,
|
|
1184
|
+
viewport: resolved.window,
|
|
1185
|
+
...resolved.fullPage ? { fullPage: true } : {},
|
|
1186
|
+
...goals.length ? { instructions: goals } : {},
|
|
1187
|
+
...use ? { use: {
|
|
1188
|
+
model: "model" in use ? use.model : "fake",
|
|
1189
|
+
moves
|
|
1190
|
+
} } : {}
|
|
1191
|
+
};
|
|
1192
|
+
a.record.artifacts = a.record.artifacts ?? [];
|
|
1193
|
+
a.record.artifacts.push({
|
|
1194
|
+
name: resolved.name,
|
|
1195
|
+
mediaType: "image/png",
|
|
1196
|
+
sha256: digest,
|
|
1197
|
+
bytes: png.length,
|
|
1198
|
+
capture
|
|
1199
|
+
});
|
|
1200
|
+
a.artifactBlobs.set(resolved.name, png);
|
|
1201
|
+
return {
|
|
1202
|
+
name: resolved.name,
|
|
1203
|
+
port: resolved.port,
|
|
1204
|
+
path: resolved.path,
|
|
1205
|
+
viewport: resolved.viewport,
|
|
1206
|
+
file
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
async function shot(a, url, resolved, remaining, onMoves) {
|
|
1210
|
+
const cache = cacheDir();
|
|
1211
|
+
return withChrome(await ensureChrome(cache, {
|
|
1212
|
+
cacheLabel: cacheLabel(cache),
|
|
1213
|
+
signal: a.signal,
|
|
1214
|
+
onProgress: (p) => a.report({
|
|
1215
|
+
kind: "browser-download",
|
|
1216
|
+
...p
|
|
1217
|
+
})
|
|
1218
|
+
}), resolved.window, remaining(), async (session) => {
|
|
1219
|
+
const result = await runJourney(session, url, {
|
|
1220
|
+
wait: resolved.wait,
|
|
1221
|
+
timeoutMs: remaining(),
|
|
1222
|
+
fullPage: resolved.fullPage,
|
|
1223
|
+
instructions: resolved.instructions,
|
|
1224
|
+
use: resolved.use,
|
|
1225
|
+
remaining,
|
|
1226
|
+
loadModel: async (name) => {
|
|
1227
|
+
const route = judgeRoute(name);
|
|
1228
|
+
return providerModel({
|
|
1229
|
+
name,
|
|
1230
|
+
route: route.kind === "direct" ? "direct" : "gateway"
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
onMoves(result.moves);
|
|
1235
|
+
const spend = captureUseSpend(result);
|
|
1236
|
+
if (spend) {
|
|
1237
|
+
const prev = a.record.captureUse;
|
|
1238
|
+
a.record.captureUse = prev ? {
|
|
1239
|
+
model: spend.model,
|
|
1240
|
+
moves: prev.moves + spend.moves,
|
|
1241
|
+
usage: addUsage(prev.usage, spend.usage)
|
|
1242
|
+
} : spend;
|
|
1243
|
+
}
|
|
1244
|
+
return result.png;
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
/** Writes PNG sidecars next to the staging eval JSON so redact never sees the bytes. */
|
|
1248
|
+
async function writeCaptureSidecars(stagingDir, recordFile, blobs) {
|
|
1249
|
+
if (blobs.size === 0) return;
|
|
1250
|
+
const dir = join(stagingDir, "evals", recordFile.replace(/\.json$/, ".artifacts"));
|
|
1251
|
+
await mkdir(dir, { recursive: true });
|
|
1252
|
+
for (const [name, png] of blobs) await writeFile(join(dir, `${name}.png`), png);
|
|
1253
|
+
}
|
|
1254
|
+
//#endregion
|
|
1255
|
+
export { asCheck as a, snapshot as c, writeCaptureSidecars as i, serveCommand as n, createChecks as o, startBackground as r, diffSinceSnapshot as s, capturePage as t };
|
|
1256
|
+
|
|
1257
|
+
//# sourceMappingURL=capture-Bp74Z6Fr.js.map
|