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