captchakraken 2.8.0 → 3.0.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 (77) hide show
  1. package/AGENTS.md +112 -0
  2. package/NOTICE +36 -0
  3. package/dist/cli-invocation.d.ts +9 -40
  4. package/dist/cli-invocation.js +16 -49
  5. package/dist/errors.d.ts +7 -27
  6. package/dist/errors.js +6 -25
  7. package/dist/humanize.d.ts +56 -240
  8. package/dist/humanize.js +159 -415
  9. package/dist/index.d.ts +5 -129
  10. package/dist/index.js +4 -98
  11. package/dist/kinds.d.ts +135 -0
  12. package/dist/kinds.js +70 -0
  13. package/dist/limits.d.ts +0 -48
  14. package/dist/limits.js +2 -48
  15. package/dist/model-name.d.ts +4 -19
  16. package/dist/model-name.js +32 -40
  17. package/dist/playwright-types.d.ts +25 -95
  18. package/dist/playwright-types.js +0 -23
  19. package/dist/puppeteer-adapter.d.ts +4 -43
  20. package/dist/puppeteer-adapter.js +22 -58
  21. package/dist/python-command.d.ts +0 -41
  22. package/dist/python-command.js +0 -37
  23. package/dist/selectors.d.ts +54 -0
  24. package/dist/selectors.js +117 -0
  25. package/dist/slide-geometry.d.ts +2 -29
  26. package/dist/slide-geometry.js +18 -44
  27. package/dist/solver.d.ts +112 -539
  28. package/dist/solver.js +1088 -2950
  29. package/dist/timing.d.ts +7 -40
  30. package/dist/timing.js +4 -36
  31. package/dist/token-usage.d.ts +1 -7
  32. package/dist/token-usage.js +10 -95
  33. package/dist/types.d.ts +77 -483
  34. package/dist/watcher.d.ts +5 -78
  35. package/dist/watcher.js +0 -56
  36. package/package.json +12 -4
  37. package/python/AGENTS.md +122 -0
  38. package/python/NOTICE +36 -0
  39. package/python/README.md +1 -2
  40. package/python/examples/README.md +10 -0
  41. package/python/examples/_harness.py +4 -73
  42. package/python/examples/demoHcaptcha.py +0 -13
  43. package/python/examples/demoRecaptcha.py +0 -10
  44. package/python/examples/watch_playwright.py +1 -26
  45. package/python/examples/with_local_gguf.py +28 -0
  46. package/python/examples/with_playwright.py +0 -19
  47. package/python/pyproject.toml +68 -19
  48. package/python/src/captchakraken/__init__.py +37 -45
  49. package/python/src/captchakraken/action_types.py +14 -36
  50. package/python/src/captchakraken/cli.py +262 -879
  51. package/python/src/captchakraken/config.py +60 -146
  52. package/python/src/captchakraken/errors.py +17 -95
  53. package/python/src/captchakraken/humanize.py +174 -597
  54. package/python/src/captchakraken/image_processor.py +16 -235
  55. package/python/src/captchakraken/keyframes.py +69 -434
  56. package/python/src/captchakraken/kinds.py +152 -0
  57. package/python/src/captchakraken/models.json +79 -21
  58. package/python/src/captchakraken/overlay.py +65 -498
  59. package/python/src/captchakraken/page_solver.py +1123 -2564
  60. package/python/src/captchakraken/planner.py +70 -428
  61. package/python/src/captchakraken/prompts.py +45 -261
  62. package/python/src/captchakraken/selectors.py +184 -0
  63. package/python/src/captchakraken/server_manager.py +7 -58
  64. package/python/src/captchakraken/solver.py +128 -522
  65. package/python/src/captchakraken/timing.py +20 -74
  66. package/python/src/captchakraken/tool_calls/board_painted.py +47 -0
  67. package/python/src/captchakraken/tool_calls/find_checkbox.py +20 -30
  68. package/python/src/captchakraken/tool_calls/find_grid.py +186 -1339
  69. package/python/src/captchakraken/tool_calls/track_piece.py +81 -57
  70. package/python/src/captchakraken/updater.py +7 -76
  71. package/python/src/captchakraken/watcher.py +12 -101
  72. package/scripts/copy-python.mjs +10 -10
  73. package/scripts/setup-python.js +1 -21
  74. package/dist/trajectory.d.ts +0 -58
  75. package/dist/trajectory.js +0 -266
  76. package/python/src/captchakraken/tool_calls/move_indicator.py +0 -431
  77. package/python/src/captchakraken/trajectory.py +0 -284
package/dist/types.d.ts CHANGED
@@ -1,498 +1,127 @@
1
- import type { Humanizer, HumanizationMode, TouchTransform } from './humanize.js';
2
- /**
3
- * Lifecycle event emitted by {@link CaptchaKrakenConfig.onStep}. One is fired
4
- * before any interaction (`stage: 'initial'`) and one after every executed
5
- * action (click batch, drag, wait, submit) plus per dynamic-grid round, so a
6
- * caller can record the exact sequence of intermediate stages, time them, and
7
- * count steps without scraping CAPTCHA_DEBUG dumps.
8
- */
1
+ import type { Humanizer, TouchTransform } from './humanize.js';
2
+ import type { ActionKind, FrameRole, HumanizationMode, PromptFamily, SolveStage, Vendor } from './kinds';
3
+ /** One intermediate stage of a solve, handed to `CaptchaKrakenConfig.onStep`. */
9
4
  export interface SolveStepEvent {
10
- /** 1-based monotonically increasing step index across the whole solve. */
5
+ /** 1-based, increasing across the whole solve. */
11
6
  index: number;
12
- /**
13
- * Coarse stage kind:
14
- * - `initial` : screenshot taken before any action (baseline)
15
- * - `click` : after a click (or batch of clicks) was executed
16
- * - `drag` : after a drag was executed (a slider counts as one)
17
- * - `type` : after a distorted-text answer was typed into its box
18
- * - `wait` : after a CLI-requested wait elapsed
19
- * - `submit` : after the Verify/Next/Submit button was clicked
20
- * - `round` : a dynamic reCAPTCHA 3x3 round boundary (pre-solve snapshot)
21
- */
22
- stage: 'initial' | 'click' | 'drag' | 'type' | 'wait' | 'submit' | 'round';
23
- /** Short human label, e.g. "round-2:clicked 3 tile(s)". */
7
+ stage: SolveStage;
24
8
  label: string;
25
- /**
26
- * Absolute path to a PNG screenshot of the captcha element at this step.
27
- * The file is owned by the callback once emitted — the solver does NOT
28
- * delete it (copy/move it where you need it). Null if the screenshot failed.
29
- */
9
+ /** PNG of the captcha element at this step; owned by the callback. Null if the screenshot failed. */
30
10
  screenshotPath: string | null;
31
- /** Detected puzzle vendor, if known. */
32
- puzzleSource?: 'hcaptcha' | 'recaptcha' | 'unknown';
33
- /**
34
- * Which captcha frame this screenshot is of:
35
- * - `checkbox` : the anchor "I'm not a robot" widget (no puzzle yet)
36
- * - `challenge` : the open image/grid challenge frame (the real puzzle)
37
- * - `unknown` : could not be determined from the frame src
38
- * Recorders that only want the actual solve (not the pre-challenge checkbox
39
- * clicks) filter to `challenge`.
40
- */
41
- frameRole?: 'checkbox' | 'challenge' | 'unknown';
42
- /** Outer solve-loop attempt this step belongs to. */
11
+ puzzleSource?: Vendor;
12
+ frameRole?: FrameRole;
43
13
  attempt: number;
44
- /** ms since solve() started. */
45
14
  elapsedMs: number;
46
- /** Free-form per-stage detail (action payload, clicked cells, etc.). */
47
15
  meta?: Record<string, any>;
48
16
  }
17
+ /** Tunables. Every field mirrors a `PageSolverConfig` field on the Python port; defaults are in SOLVE_DEFAULTS. */
49
18
  export interface CaptchaKrakenConfig {
50
- /**
51
- * How the driver MOVES — a choice of input DEVICE, not a realism dial.
52
- *
53
- * - `'mouse'` (default) Bezier arcs, Fitts's-law durations, overshoot.
54
- * - `'mobile'` touch events with finger kinematics. On a touch-only widget
55
- * this is the difference between the page's handlers firing and
56
- * not; a mousemove there is the wrong event, not a weaker one.
57
- * Needs a Chromium-family page launched with `hasTouch: true`,
58
- * or an Appium/WebdriverIO driver in {@link touchDriver}.
59
- * - `'none'` the shortest legal path to the same DOM effect. Much faster,
60
- * and detectable by anything scoring pointer telemetry — for
61
- * fixtures, self-hosted targets, and stacks that humanise
62
- * elsewhere.
63
- *
64
- * Unset falls back to `CAPTCHA_HUMANIZATION`, then `'mouse'`. Set
65
- * {@link humanizer} instead to supply your own implementation.
66
- *
67
- * @example
68
- * ```typescript
69
- * const context = await browser.newContext({ ...devices['Pixel 7'], hasTouch: true });
70
- * const solver = new CaptchaKrakenSolver({ humanization: 'mobile' });
71
- * await solver.solve(await context.newPage());
72
- * ```
73
- */
19
+ /** Input device; default `mouse`. Falls back to CAPTCHA_HUMANIZATION. */
74
20
  humanization?: HumanizationMode;
75
- /**
76
- * Your own {@link Humanizer}. Overrides {@link humanization} entirely — the
77
- * driver then makes no decisions about pointer motion at all.
78
- *
79
- * Use it when you already model your users' input (a hardware pointer, a
80
- * device farm, a recorded trace), or when the browser humanises for you:
81
- * camoufox's `humanize` juggler re-humanises every `mouse.move()` it is
82
- * handed, and composing both measured 82.1s against 13.4s on one solve.
83
- *
84
- * @example
85
- * ```typescript
86
- * import { NullHumanizer, type Humanizer } from 'captchakraken';
87
- *
88
- * class MyPointer extends NullHumanizer implements Humanizer {
89
- * async move(page, to) { await myHardwareMouse.glideTo(to[0], to[1]); this.at = to; }
90
- * }
91
- * new CaptchaKrakenSolver({ humanizer: new MyPointer() });
92
- * ```
93
- */
21
+ /** Your own gesture implementation; overrides `humanization`. */
94
22
  humanizer?: Humanizer;
95
- /**
96
- * `humanization: 'mobile'` only. The thing that is actually TOUCHED, when it
97
- * is not the page object — an Appium / WebdriverIO / Selenium driver on a real
98
- * handset. Left unset, the mode dispatches CDP touch events at the page it
99
- * was given, which is what browser mobile emulation wants.
100
- */
23
+ /** Mobile only: an Appium / WebdriverIO driver on a real handset. Unset dispatches CDP touch at the page. */
101
24
  touchDriver?: any;
102
- /**
103
- * `humanization: 'mobile'` only. CSS-pixel → device-pixel transform for
104
- * {@link touchDriver}. `scale` is usually `window.devicePixelRatio`; `origin`
105
- * is the top-left of the webview in screen coordinates. The default identity
106
- * is right for emulation and for callers who map coordinates themselves.
107
- */
25
+ /** Mobile only: CSS-pixel to device-pixel transform for `touchDriver`. */
108
26
  touchTransform?: TouchTransform;
109
- /**
110
- * Optional observer fired at each intermediate solve stage. Receives a
111
- * baseline screenshot before any action and one after every executed action.
112
- * Use it to capture intermediate-stage screenshots, count steps, and time
113
- * each phase. The callback may be async; the solver awaits it. Errors thrown
114
- * by the callback are swallowed (never fail a solve because logging failed).
115
- *
116
- * The PNG at `event.screenshotPath` is owned by the callback once emitted —
117
- * the solver will not delete it.
118
- */
27
+ /** Observer fired at each intermediate stage; errors are swallowed. */
119
28
  onStep?: (event: SolveStepEvent) => void | Promise<void>;
120
- /**
121
- * Path to the bundled CaptchaKraken CLI root.
122
- *
123
- * Usually you do NOT need to set this. If omitted, the solver will auto-resolve the
124
- * `python/` directory (the captchakraken package) shipped inside this npm package.
125
- */
29
+ /** Path to the bundled Python engine. Auto-resolved when omitted. */
126
30
  repoPath?: string;
127
- /**
128
- * Command to run python (default: 'python' or 'python3').
129
- */
31
+ /** Python interpreter for the engine. Default: the bundled venv, else `python3`, else `python`. */
130
32
  pythonCommand?: string;
131
- /**
132
- * vLLM LoRA name to invoke.
133
- *
134
- * Defaults to whatever `models.json` calls `latest` (via CAPTCHA_LORA_NAME if
135
- * set) — NOT a literal. The name selects the PROMPT GENERATION as well as the
136
- * weights, so a constant here would pin the prompts to one generation while
137
- * `latest` moved on. Override only if you've registered a different module
138
- * with the vLLM server; see model-name.ts.
139
- */
33
+ /** Served model name. Default: resolved from models.json for the endpoint in use. */
140
34
  model?: string;
141
- /**
142
- * Force ONE expert of a ROUTED model — 'pixel' | 'grid' | 'video' | 'text'.
143
- *
144
- * A routed model (Abyss) is four LoRA adapters behind one endpoint, and the
145
- * router is the prompt family each request is about to send — so leaving this
146
- * unset already reaches the right expert. This is the override: serve one
147
- * arm, drive only the puzzles it owns, which is what a per-arm benchmark
148
- * needs and what a licence holder pinning a single expert wants.
149
- *
150
- * Refused against a model that serves a single adapter — every model
151
- * published so far — rather than ignored, because a run that quietly measured
152
- * the generalist while reporting an expert is a number nobody can catch.
153
- *
154
- * Mirrors the Python `PageSolverConfig.expert`; also settable as
155
- * CAPTCHA_EXPERT, which this forwards to the CLI ahead of.
156
- */
157
- expert?: string;
158
- /**
159
- * Bearer token for the vLLM server (also picked up from VLLM_API_KEY env).
160
- */
35
+ /** Force one expert of a routed model. Also CAPTCHA_EXPERT. */
36
+ expert?: PromptFamily;
37
+ /** Bearer token; also read from CAPTCHA_KRAKEN_API_KEY / VLLM_API_KEY. */
161
38
  apiKey?: string;
162
- /**
163
- * Starting mouse position (default: { x: 100, y: 100 }).
164
- * HIGHLY RECOMMENDED to set this, prevents jumping around of the cursor when solving.
165
- */
39
+ /** Where the pointer starts. Default { x: 100, y: 100 }. */
166
40
  startingMousePosition?: {
167
41
  x: number;
168
42
  y: number;
169
43
  };
170
- /**
171
- * Automatically re-check for newly opened / next-step captchas after each solve
172
- * attempt (e.g., clicking a checkbox opens an image challenge).
173
- *
174
- * The loop count that FITS inside overallSolveTimeoutMs, rather than one that
175
- * needs policing by the clock: a round costs ~4-7s, so six rounds is the 45s
176
- * budget. A backstop, not the normal way a hopeless solve ends — that is
177
- * maxNoProgressRounds. Reaching this number is a bug report.
178
- *
179
- * Default: 6
180
- */
44
+ /** Solve rounds that fit the timeout. Default 6. */
181
45
  maxSolveLoops?: number;
182
- /**
183
- * Consecutive rounds producing the SAME answer before the solve is abandoned.
184
- *
185
- * At temperature 0 the model is a function of the picture, so an identical
186
- * answer means an identical picture — and every answer this driver produces is
187
- * EXECUTED, so the previous one already ran and moved nothing. Repeating it
188
- * cannot do better; it just spends a round.
189
- *
190
- * 2 rather than 1 because the first repeat also escalates to a RECORDING (see
191
- * shouldRetryAsAnimated), which is the one recovery worth trying: a cycling
192
- * board reads as a still and answers the same way every round. One more round
193
- * buys that chance.
194
- *
195
- * Default: 2
196
- */
46
+ /** Consecutive identical answers before the solve is abandoned. Default 2. */
197
47
  maxNoProgressRounds?: number;
198
- /**
199
- * Delay (ms) after executing actions before re-detecting captchas.
200
- * Useful to allow challenge frames / new images to appear.
201
- *
202
- * Default: 1200
203
- */
48
+ /** Dwell after a round that answered nothing. Default 1200. */
204
49
  postSolveDelayMs?: number;
205
- /**
206
- * Overall time limit (ms) for the entire solve loop.
207
- *
208
- * Default: 45000
209
- */
50
+ /** Whole-solve time limit. Default 45000. */
210
51
  overallSolveTimeoutMs?: number;
211
- /**
212
- * Poll interval (ms) for the reCAPTCHA grid-cell-load wait — how often the
213
- * solver re-screenshots the grid while waiting for tiles to stop fading in.
214
- * Doubles as the inter-frame gap for the settle change-detector, so keep it
215
- * comfortably above zero.
216
- *
217
- * Default: 250
218
- */
52
+ /** reCAPTCHA grid-load poll interval. Default 250. */
219
53
  gridLoadPollIntervalMs?: number;
220
- /**
221
- * Overall timeout (ms) for the reCAPTCHA grid-cell-load wait. On timeout the
222
- * solver proceeds to screenshot anyway (best-effort).
223
- *
224
- * Default: 8000
225
- */
54
+ /** reCAPTCHA grid-load timeout. Default 8000. */
226
55
  gridLoadTimeoutMs?: number;
227
- /**
228
- * reCAPTCHA 3x3 dynamic puzzles only. After clicking a round of tiles, the
229
- * max time (ms) to wait for at least one clicked blank/fading tile to finish
230
- * loading before re-screenshotting and re-solving. On timeout the solver
231
- * proceeds anyway (best-effort, backstopped by overallSolveTimeoutMs).
232
- *
233
- * Default: 6000
234
- */
56
+ /** Board-paint gate poll interval. Default 180. */
57
+ boardPaintPollMs?: number;
58
+ /** How long to wait for a widget to paint its puzzle. Default 2500. */
59
+ boardPaintTimeoutMs?: number;
60
+ /** Share of the panel's centre that must carry structure. Default: the engine's own. */
61
+ boardPaintFloor?: number;
62
+ /** reCAPTCHA 3x3: how long to wait for a clicked tile to reload. Default 6000. */
235
63
  recaptchaDynamicFadeWaitMs?: number;
236
- /**
237
- * reCAPTCHA 3x3 dynamic puzzles only. Minimum gap (ms) between the two frames
238
- * the fade detectors diff, and the poll cadence for waitForAnyClickedTileLoaded
239
- * / currentLoadingCells. Kept comfortably above zero so two consecutive frames
240
- * during a slow fade differ enough for the change detector to fire (frames
241
- * captured back-to-back can look identical mid-fade and read as "loaded").
242
- *
243
- * Default: 250
244
- */
64
+ /** reCAPTCHA 3x3: gap between the two frames the fade detectors diff. Default 250. */
245
65
  recaptchaDynamicFadePollMs?: number;
246
- /**
247
- * reCAPTCHA 3x3 dynamic puzzles only. Grace window (ms) after clicking during
248
- * which the solver watches the clicked tiles for the ONSET of a blank/fade
249
- * before deciding the puzzle is solved. reCAPTCHA keeps a clicked tile
250
- * SELECTED (showing its old image + a blue badge) for a couple of seconds and
251
- * only THEN blanks it to swap in a replacement, so the window must comfortably
252
- * exceed that delay or we submit while the refresh is still pending. If no
253
- * clicked tile goes blank/changing within this window, the puzzle is treated
254
- * as solved.
255
- *
256
- * Default: 4000
257
- */
66
+ /** reCAPTCHA 3x3: grace window after a click for the fade to begin. Default 4000. A window, not a snapshot: a clicked tile stays selected 1-3s and only then blanks. */
258
67
  recaptchaFadeOnsetGraceMs?: number;
259
- /**
260
- * reCAPTCHA 3x3 dynamic puzzles only. Cap on the number of
261
- * click → refresh → re-solve rounds within a single puzzle, independent of
262
- * maxSolveLoops.
263
- *
264
- * Default: 8
265
- */
68
+ /** reCAPTCHA 3x3: cap on click/refresh rounds per puzzle. Default 8. */
266
69
  recaptchaMaxDynamicRounds?: number;
267
- /**
268
- * reCAPTCHA 3x3 dynamic puzzles only. When true, the solver hovers the mouse
269
- * over the just-clicked blank/fading tiles (in click order) while waiting for
270
- * them to reload, mimicking a human. Disable to skip the hover behavior.
271
- *
272
- * Default: true
273
- */
70
+ /** reCAPTCHA 3x3: hover the reloading tiles while waiting. Default true. */
274
71
  recaptchaTileHoverEnabled?: boolean;
275
- /**
276
- * While the model is generating a solution (the main idle window), drift the
277
- * cursor over the challenge area with human-like trajectories instead of
278
- * leaving it frozen. Cancelled the instant the model responds. Set false to
279
- * keep the cursor still during inference.
280
- *
281
- * Default: true
282
- */
72
+ /** Drift the cursor over the challenge while the model thinks. Default true. */
283
73
  idleMouseWander?: boolean;
284
- /**
285
- * After an action, the max time (ms) to watch for the solve outcome — the
286
- * vendor's solved signal (checkbox checked / response token) or a freshly
287
- * rendered next round — before falling back to a full re-detect. Returning as
288
- * soon as the solved signal appears avoids re-entering the solve pipeline on a
289
- * challenge frame that is merely animating closed.
290
- *
291
- * That is the window's whole job, and it is why a WRONG answer spends all of
292
- * it: the vendors emit nothing on a wrong answer, they just re-deal. So
293
- * sizing it means asking how late a real SUCCESS can arrive — measured over
294
- * 34 successful rounds across 20 puzzle types on the fixture server: p50
295
- * 360ms, max 528ms. 1000ms is that worst case plus headroom for a vendor
296
- * round-trip nobody has measured yet. Down from 2500, which was spent in full
297
- * on every unsuccessful round.
298
- *
299
- * Default: 1000
300
- */
74
+ /** How long to watch for the vendor's verdict after an action. Default 1000. */
301
75
  postSolveOutcomeTimeoutMs?: number;
302
- /**
303
- * Poll interval (ms) inside that window.
304
- *
305
- * The success signals are polled, so detection latency is a MULTIPLE of this.
306
- * Each poll is a couple of cheap DOM reads.
307
- *
308
- * Default: 75
309
- */
76
+ /** Poll interval inside that window. Default 75. */
310
77
  postSolveOutcomePollMs?: number;
311
- /**
312
- * Guard against the captcha frame changing WHILE the model is generating.
313
- * reCAPTCHA/hCaptcha fade fresh tiles in over ~1s; if new imagery paints
314
- * between the screenshot we send the model and the moment its answer returns,
315
- * that answer describes a stale ("undeveloped") frame and its tile picks /
316
- * bboxes no longer line up with what's on screen. When enabled, the solver
317
- * re-screenshots the frame after inference, and if it changed it discards the
318
- * stale answer and re-solves on the fresh frame (see maxStaleFrameReSolves).
319
- *
320
- * Default: true
321
- */
78
+ /** Re-solve when the frame changed during inference. Default true. */
322
79
  staleFrameReSolveEnabled?: boolean;
323
- /**
324
- * Fraction of pixels (0–1) that must differ between the frame sent to the
325
- * model and a screenshot taken right after inference for the frame to count
326
- * as "changed during inference" (a tile faded in / refreshed). Below this,
327
- * the frame is treated as unchanged and the answer is used as-is. Reuses the
328
- * same frame-diff primitive as the reCAPTCHA settle detector.
329
- *
330
- * Default: 0.02
331
- */
80
+ /** Fraction of pixels that must differ to count as changed. Default 0.02. */
332
81
  staleFrameDiffThreshold?: number;
333
- /**
334
- * Max number of times to re-screenshot + re-solve when the frame keeps
335
- * changing during inference, before giving up and acting on the latest answer
336
- * (better to act than to spin). Set 0 to detect-and-log without re-solving.
337
- *
338
- * Default: 2
339
- */
82
+ /** Re-solves allowed per round. Default 2. */
340
83
  maxStaleFrameReSolves?: number;
341
- /**
342
- * Fraction of pixels (0–1) that must differ between two consecutive challenge
343
- * frames for it to still count as "moving" (loading/animating) rather than
344
- * settled.
345
- *
346
- * Default: 0.01
347
- */
84
+ /** Settle monitor: fraction of pixels that still count as moving. Default 0.01. */
348
85
  settleDiffThreshold?: number;
349
- /**
350
- * How long to wait for hCaptcha's task images to paint before screenshotting
351
- * anyway. Best-effort by design — the screenshot happens either way — so it
352
- * is bounded by what a loading tile plausibly costs. Default: 3000
353
- */
86
+ /** How long to wait for hCaptcha's task images to paint. Default 3000. */
354
87
  hcaptchaImagesTimeoutMs?: number;
355
- /** Poll interval (ms) for the settle monitor. Default: 220 */
88
+ /** Settle monitor poll interval. Default 220. */
356
89
  settlePollMs?: number;
357
- /**
358
- * Consecutive still frame-pairs required before the challenge is declared
359
- * settled and safe to screenshot for the model.
360
- *
361
- * Default: 2
362
- */
90
+ /** Consecutive still frame pairs that mean settled. Default 2. */
363
91
  settleFrames?: number;
364
- /** Overall timeout (ms) for the settle monitor before it gives up. Default: 9000 */
92
+ /** Settle monitor timeout. Default 9000. */
365
93
  settleTimeoutMs?: number;
366
- /**
367
- * If the challenge keeps changing continuously for at least this long without
368
- * ever settling, it's treated as an **animated / video** challenge (surfaced
369
- * distinctly, not as "unsupported"). Static grids settle in ~1–2s; a video
370
- * never does.
371
- *
372
- * Default: 4500
373
- */
94
+ /** Still moving after this long means animated. Default 4500. */
374
95
  animatedChallengeAfterMs?: number;
375
- /**
376
- * Record and solve animated challenges. When false, a challenge that never
377
- * settles fails with `.animated = true` as it did before — for callers who
378
- * would rather fail fast than spend the recording time.
379
- *
380
- * Default: true
381
- */
96
+ /** Record and solve animated challenges from keyframes. Default true. */
382
97
  videoSolveEnabled?: boolean;
383
- /**
384
- * Length (ms) and frame rate of the burst recorded from an animated challenge.
385
- *
386
- * Do not tune these casually. They are deliberately identical to the geometry
387
- * the training corpus was collected at (4000ms @ 10fps), and the keyframe
388
- * slicer reads the clip's temporal structure — a different length or rate
389
- * changes where a cycle's period lands across the frames, so the live set gets
390
- * sliced differently from the trained set and the model's frame number stops
391
- * meaning what the driver thinks it means.
392
- *
393
- * Defaults: 4000 / 10
394
- */
98
+ /** Burst floor. Default 4000; matches the training corpus. */
395
99
  videoBurstDurationMs?: number;
100
+ /** Burst frame rate. Default 10; matches the training corpus. */
396
101
  videoBurstFps?: number;
397
- /**
398
- * How long (ms) to wait for the widget to return to the keyframe the model
399
- * chose, before clicking anyway; and the poll interval while waiting.
400
- *
401
- * Bounded because the alternative is worse: these puzzles cycle, so the state
402
- * does come back — but if the recording caught a one-off transition it never
403
- * will, and a click on the model's coordinates is a better use of the
404
- * remaining budget than a timeout.
405
- *
406
- * Defaults: 6000 / 120
407
- */
102
+ /** How long to hold a click waiting for the chosen keyframe. Default 9000: GeeTest svg dwells up to 2.7s a screen, so a 3-screen cycle is 8.1s worst case and 6s gave up one screen short. */
408
103
  keyframeWaitTimeoutMs?: number;
104
+ /** Poll interval while waiting. Default 120. */
409
105
  keyframeWaitPollMs?: number;
410
- /**
411
- * Extra wall clock (ms) granted ONCE, the first time a solve escalates to a
412
- * recording. NOT a looser `overallSolveTimeoutMs`.
413
- *
414
- * That budget counts ROUNDS — `maxSolveLoops` x ~7000ms, which
415
- * no-progress.test.ts pins. A recording is not a round: it is a fixed extra
416
- * stage costing the burst, the slice, one MULTI-IMAGE inference (six
417
- * keyframes, several times a still's) and the wait for the widget to come
418
- * back round to the chosen frame. Nothing in the 45s was set aside for it, so
419
- * an escalation late in a solve simply ran the clock out and reported a
420
- * timeout — which reads as a slow model rather than as a budget with no room
421
- * for what the solver had just decided to do.
422
- *
423
- * Measured 2026-08-22, Tier 3 run 32596340560: hcaptcha_click_image_by_traits,
424
- * hcaptcha_connect_path and hcaptcha_grid_3x3_property each failed this port
425
- * with "Captcha solve timed out after 45000ms (attempt 6/6)" at 49-59s, and
426
- * solve in 14-20s on the rounds the still path answers them.
427
- *
428
- * The total granted is `videoBurstDurationMs + keyframeWaitTimeoutMs + this`,
429
- * derived so a longer burst carries its own budget — page_solver.py's
430
- * `video_budget_ms` is the same arithmetic, since the two ports must not
431
- * disagree about how long a video solve may take.
432
- *
433
- * Granted only when `videoSolveEnabled`: a caller who wants a hard deadline
434
- * turns recording off, which already means "fail fast rather than spend the
435
- * recording time".
436
- *
437
- * Default: 8000
438
- */
106
+ /** Burst ceiling for a board that never repeats a screen. Default 12000. */
107
+ videoBurstMaxMs?: number;
108
+ /** Consecutive moved settle polls that mean animated. Default 5. */
109
+ animatedMotionStreak?: number;
110
+ /** Record while the still is being read, so a cycling board is known before its answer runs. Default true. */
111
+ speculativeBurstEnabled?: boolean;
112
+ /** Extra wall clock granted once when a solve escalates to a recording. Default 8000. */
439
113
  videoExtraInferenceMs?: number;
440
- /**
441
- * After clicking Submit/Verify, the solver EXPECTS the frame to change (advance
442
- * to the next round, or close because it was accepted). This is how long (ms)
443
- * to wait for that transition to begin before re-evaluating — so the shift
444
- * itself is never screenshotted and mis-read as a fresh (blank) puzzle.
445
- *
446
- * Default: 4000
447
- */
114
+ /** After a submit, how long to wait for the frame to change. Default 4000. */
448
115
  postSubmitChangeTimeoutMs?: number;
449
- /**
450
- * Per-call timeout (ms) for element screenshots of the challenge iframe.
451
- * Playwright's default is 30000, which means a stale/transitioning handle
452
- * (e.g. hCaptcha swapped in the next round while we held the old iframe)
453
- * hangs the whole solve for 30s before failing. Bounding it lets a stale
454
- * handle fail fast so the solve loop can re-detect the fresh challenge.
455
- *
456
- * Default: 8000
457
- */
116
+ /** Per-call timeout for element screenshots. Default 8000. */
458
117
  elementScreenshotTimeoutMs?: number;
459
- /**
460
- * How long an `onStep` observer's screenshot may take, ms (default 2000).
461
- *
462
- * Deliberately separate from `elementScreenshotTimeoutMs`, which is sized for
463
- * the picture the MODEL reads. This one is a trace snapshot: it is taken with
464
- * `animations: 'disabled'`, which makes Playwright wait for the element to
465
- * stop moving first, and on a still-animating widget that ran to the full 8s
466
- * on every step — 8.0s of a measured 12.0s solve. An observer must never cost
467
- * more than the action it observes, and a missed frame in a trace costs
468
- * nothing. Ignored entirely when no `onStep` is set.
469
- */
118
+ /** Timeout for an `onStep` screenshot. Default 2000. */
470
119
  stepScreenshotTimeoutMs?: number;
471
- /**
472
- * After a submit, hCaptcha may replace the challenge iframe for the next
473
- * round, detaching the handle we just detected ("element is not visible").
474
- * This is a transition, not a dead puzzle: how many times to back off,
475
- * re-detect the fresh challenge, and retry before giving up.
476
- *
477
- * Default: 3
478
- */
120
+ /** Re-detect retries after a stale handle. Default 3. */
479
121
  maxStaleElementRetries?: number;
480
- /**
481
- * Backoff (ms) before re-detecting after a stale-element screenshot failure,
482
- * giving the round transition time to finish.
483
- *
484
- * Default: 900
485
- */
122
+ /** Backoff before re-detecting. Default 900. */
486
123
  staleElementBackoffMs?: number;
487
- /**
488
- * When the model reports "unsupported" *after we've already interacted* (i.e.
489
- * mid multi-round), it's almost always a not-yet-settled next round rather
490
- * than a genuinely unsupported puzzle. This is how many times to wait for the
491
- * challenge to settle and re-solve before giving up. (A single retry loses a
492
- * race when the next round loads slowly.)
493
- *
494
- * Default: 3
495
- */
124
+ /** Retries when "unsupported" arrives mid-solve on a transitional frame. Default 3. */
496
125
  maxUnsupportedReSolves?: number;
497
126
  }
498
127
  export interface BoundingBox {
@@ -501,61 +130,34 @@ export interface BoundingBox {
501
130
  2: number;
502
131
  3: number;
503
132
  }
504
- /**
505
- * Fields present ONLY on actions from an animated challenge.
506
- *
507
- * Why an action carries a picture: on an animated puzzle the target is visible
508
- * only part of the time, so the coordinates are correct only while the widget
509
- * looks the way it did in that keyframe. The driver holds the mouse until the
510
- * live neighbourhood around the click point matches the same neighbourhood of
511
- * `await_keyframe`, then clicks. Without the wait, a click on a fading sprite
512
- * lands on background.
513
- *
514
- * Both are set together or not at all — a number with no image cannot be waited
515
- * on, and an image with no number cannot be reported. Absent on every still
516
- * puzzle, where there is no moment to wait for.
517
- */
133
+ /** Present only on actions from an animated challenge: the keyframe the answer was read off. */
518
134
  export interface AnimatedActionFields {
519
- /** Absolute path to the keyframe the model chose to act on. */
520
135
  await_keyframe?: string | null;
521
- /** Its 1-based number in the keyframe set that was sent. */
522
136
  frame?: number | null;
523
137
  }
524
138
  export interface ClickAction extends AnimatedActionFields {
525
- action: 'click';
526
- /**
527
- * One or more normalized [x1, y1, x2, y2] bboxes (0–1 fractions of the
528
- * screenshot). Each entry produces one click. Emitted by v2 CLI for both
529
- * grid selections and click-puzzle points.
530
- */
139
+ action: typeof ActionKind.CLICK;
140
+ /** Normalised [x1, y1, x2, y2] boxes, one click each. */
531
141
  target_bounding_boxes?: Array<[number, number, number, number]>;
532
- /** Legacy v1 fields kept for backwards-compat with older CLI builds. */
533
142
  target_number?: number | null;
534
143
  target_bounding_box?: [number, number, number, number] | null;
535
144
  target_coordinates?: [number, number] | null;
536
145
  }
537
146
  export interface DragAction extends AnimatedActionFields {
538
- action: 'drag';
539
- /**
540
- * Null on a PUZZLE-PIECE SLIDER. Not a missing field — the shape of the
541
- * answer: what you grab (a handle, elsewhere on the widget) is not what has
542
- * to arrive (the piece), and how far one moves the other is a vendor detail
543
- * no picture reveals. The driver reads a null source as "find the slider and
544
- * close the loop on the piece" (see executeSlide).
545
- */
147
+ action: typeof ActionKind.DRAG;
148
+ /** Null on a puzzle-piece slider: the driver finds the handle and closes the loop on the piece. */
546
149
  source_bounding_box: [number, number, number, number] | null;
547
150
  target_bounding_box: [number, number, number, number];
548
151
  }
549
- /** Distorted-text captchas: the answer is a string, not a place. */
550
152
  export interface TypeAction {
551
- action: 'type';
153
+ action: typeof ActionKind.TYPE;
552
154
  text: string;
553
155
  }
554
156
  export interface DoneAction {
555
- action: 'done';
157
+ action: typeof ActionKind.DONE;
556
158
  }
557
159
  export interface WaitAction {
558
- action: 'wait';
160
+ action: typeof ActionKind.WAIT;
559
161
  duration_ms: number;
560
162
  }
561
163
  export type CaptchaAction = ClickAction | WaitAction | DragAction | TypeAction | DoneAction;
@@ -584,14 +186,6 @@ export interface SolveResult {
584
186
  cachedInputTokens: number;
585
187
  estimatedCost: number;
586
188
  };
587
- /**
588
- * Where this solve's wall-clock went, in milliseconds per phase — the same
589
- * partition `CAPTCHA_TIMINGS=1` prints, handed to the caller instead of to
590
- * stderr. Mirrors `SolveResult.phases` on the Python port.
591
- *
592
- * Returned rather than only logged because "the solve took 12s" is not
593
- * actionable and "the settle monitor spent 4s of it" is, and a caller
594
- * measuring a fleet cannot scrape another process's stderr.
595
- */
189
+ /** Milliseconds per phase, plus `total`. */
596
190
  phases?: Record<string, number>;
597
191
  }