captchakraken 2.4.0 → 2.6.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 (51) hide show
  1. package/LICENSE +47 -7
  2. package/README.md +24 -4
  3. package/dist/cli-invocation.d.ts +46 -0
  4. package/dist/cli-invocation.js +68 -0
  5. package/dist/index.d.ts +27 -0
  6. package/dist/index.js +29 -1
  7. package/dist/limits.d.ts +51 -0
  8. package/dist/limits.js +54 -0
  9. package/dist/model-name.d.ts +23 -0
  10. package/dist/model-name.js +70 -0
  11. package/dist/playwright-types.d.ts +28 -0
  12. package/dist/puppeteer-adapter.d.ts +11 -0
  13. package/dist/puppeteer-adapter.js +23 -0
  14. package/dist/python-command.d.ts +50 -0
  15. package/dist/python-command.js +60 -0
  16. package/dist/slide-geometry.d.ts +33 -0
  17. package/dist/slide-geometry.js +64 -0
  18. package/dist/solver.d.ts +214 -12
  19. package/dist/solver.js +1285 -165
  20. package/dist/trajectory.d.ts +31 -0
  21. package/dist/trajectory.js +162 -0
  22. package/dist/types.d.ts +128 -11
  23. package/dist/watcher.d.ts +99 -0
  24. package/dist/watcher.js +129 -0
  25. package/package.json +2 -5
  26. package/python/LICENSE +47 -7
  27. package/python/README.md +25 -8
  28. package/python/examples/README.md +36 -0
  29. package/python/examples/_harness.py +182 -94
  30. package/python/examples/demoHcaptcha.py +2 -1
  31. package/python/examples/demoRecaptcha.py +2 -1
  32. package/python/examples/watch_playwright.py +56 -0
  33. package/python/examples/with_playwright.py +43 -0
  34. package/python/pyproject.toml +4 -3
  35. package/python/src/captchakraken/__init__.py +5 -3
  36. package/python/src/captchakraken/action_types.py +24 -3
  37. package/python/src/captchakraken/cli.py +212 -2
  38. package/python/src/captchakraken/config.py +39 -5
  39. package/python/src/captchakraken/keyframes.py +690 -0
  40. package/python/src/captchakraken/models.json +120 -0
  41. package/python/src/captchakraken/page_solver.py +1465 -102
  42. package/python/src/captchakraken/pinned_model.json +5 -7
  43. package/python/src/captchakraken/planner.py +253 -59
  44. package/python/src/captchakraken/prompts.py +485 -0
  45. package/python/src/captchakraken/solver.py +182 -11
  46. package/python/src/captchakraken/timing.py +70 -0
  47. package/python/src/captchakraken/tool_calls/find_grid.py +367 -52
  48. package/python/src/captchakraken/tool_calls/track_piece.py +96 -0
  49. package/python/src/captchakraken/trajectory.py +2 -7
  50. package/python/src/captchakraken/watcher.py +193 -0
  51. package/python/src/captchakraken/planner_types.py +0 -74
@@ -15,6 +15,7 @@
15
15
  * - `handle.getAttribute(name)` → `handle.evaluate((el,n)=>el.getAttribute(n), name)`
16
16
  * - `handle.textContent()` → `handle.evaluate(el=>el.textContent)`
17
17
  * - `handle.scrollIntoViewIfNeeded()`→ `handle.scrollIntoView()`
18
+ * - `page.isClosed()` → same, but must be forwarded explicitly
18
19
  * Everything else (`$`, `$$`, `$eval`, `waitForFunction`, `mouse.*`,
19
20
  * `screenshot({path})`, `boundingBox`, `contentFrame`, `isVisible`) is
20
21
  * call-compatible and passed straight through.
@@ -62,6 +63,7 @@ function wrapHandle(h) {
62
63
  getAttribute: (name) => h.evaluate((el, n) => el.getAttribute(n), name),
63
64
  isVisible: () => h.isVisible(),
64
65
  textContent: () => h.evaluate((el) => el.textContent),
66
+ $: async (selector) => wrapHandle(await h.$(selector)),
65
67
  };
66
68
  }
67
69
  function wrapFrame(f) {
@@ -86,11 +88,32 @@ function fromPuppeteer(page) {
86
88
  down: (options) => page.mouse.down(options),
87
89
  up: (options) => page.mouse.up(options),
88
90
  },
91
+ keyboard: {
92
+ type: (text, options) => page.keyboard.type(text, options),
93
+ // Puppeteer has no combo syntax: 'Control+A' must be held, pressed,
94
+ // released. Playwright accepts the combo string directly, which is why
95
+ // the solver speaks Playwright and this adapter translates.
96
+ press: async (key) => {
97
+ const parts = key.split('+');
98
+ const target = parts.pop();
99
+ for (const mod of parts)
100
+ await page.keyboard.down(mod);
101
+ await page.keyboard.press(target);
102
+ for (const mod of parts.reverse())
103
+ await page.keyboard.up(mod);
104
+ },
105
+ },
89
106
  waitForTimeout: (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)),
90
107
  waitForSelector: async (selector, options) => wrapHandle(await page.waitForSelector(selector, toPuppeteerSelectorOptions(options))),
91
108
  viewportSize: () => page.viewport(),
92
109
  $: async (selector) => wrapHandle(await page.$(selector)),
93
110
  $$: async (selector) => (await page.$$(selector)).map((h) => wrapHandle(h)).filter(Boolean),
94
111
  $eval: (selector, pageFunction, arg) => page.$eval(selector, pageFunction, arg),
112
+ // Same name and shape on both libraries, but it still has to be forwarded:
113
+ // the wrapper is a fresh object literal, so anything not listed here is
114
+ // simply absent. The auto-solve watcher reads it to end its loop when the
115
+ // caller closes the browser, and an undefined `isClosed` reads as "still
116
+ // open" — a watcher that polls a dead page forever.
117
+ isClosed: () => page.isClosed(),
95
118
  };
96
119
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which interpreter runs the bundled CaptchaKraken CLI.
3
+ *
4
+ * This used to be `pythonCommand = 'python'`, inline in solver.ts. On
5
+ * Debian/Ubuntu — the most common Linux deployment target, and what CI runs —
6
+ * there is no `python` on PATH at all, only `python3`. So every JS solve died
7
+ * with `/bin/sh: 1: python: not found` before it ever reached the model, unless
8
+ * a `.venv` happened to exist next to the package. The type doc claimed the
9
+ * default was "'python' or 'python3'"; the code never tried `python3`.
10
+ *
11
+ * That is a straight violation of the rule that the Python and JS clients
12
+ * behave identically: the Python client obviously ran, so the divergence looked
13
+ * like a model or endpoint problem rather than a missing binary.
14
+ *
15
+ * Kept in its own module, with the probe injected, so the resolution order is
16
+ * testable without spawning anything.
17
+ */
18
+ /** Env var that overrides everything below it. */
19
+ export declare const PYTHON_ENV_VAR = "CAPTCHA_KRAKEN_PYTHON";
20
+ /**
21
+ * Candidates tried in order when nothing is configured.
22
+ *
23
+ * `python3` FIRST. On Debian-family systems `python` is absent; where both
24
+ * exist they are the same interpreter in practice, and PEP 394 makes `python3`
25
+ * the portable spelling. `python` stays as the fallback for Windows and for
26
+ * venvs that only expose the unsuffixed name.
27
+ */
28
+ export declare const PYTHON_CANDIDATES: readonly ["python3", "python"];
29
+ export interface ResolveOptions {
30
+ /** `config.pythonCommand`, if the caller set one. Wins outright. */
31
+ configured?: string;
32
+ /** The venv interpreter, when one was found next to the CLI. Wins over probing. */
33
+ venvPython?: string | null;
34
+ /** Process env, injected for testability. */
35
+ env?: NodeJS.ProcessEnv;
36
+ /** Returns true if the command is runnable. Injected so tests never spawn. */
37
+ exists?: (command: string) => boolean;
38
+ }
39
+ /**
40
+ * Resolve the interpreter, most specific first:
41
+ *
42
+ * 1. an explicitly configured `pythonCommand`
43
+ * 2. `CAPTCHAKRAKEN_PYTHON`
44
+ * 3. the venv interpreter beside the bundled CLI
45
+ * 4. the first of `python3`, `python` that actually exists
46
+ *
47
+ * Falls back to `python3` rather than `python` when nothing can be probed: on
48
+ * the platform where the two differ, `python3` is the one that is present.
49
+ */
50
+ export declare function resolvePythonCommand(options?: ResolveOptions): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Which interpreter runs the bundled CaptchaKraken CLI.
4
+ *
5
+ * This used to be `pythonCommand = 'python'`, inline in solver.ts. On
6
+ * Debian/Ubuntu — the most common Linux deployment target, and what CI runs —
7
+ * there is no `python` on PATH at all, only `python3`. So every JS solve died
8
+ * with `/bin/sh: 1: python: not found` before it ever reached the model, unless
9
+ * a `.venv` happened to exist next to the package. The type doc claimed the
10
+ * default was "'python' or 'python3'"; the code never tried `python3`.
11
+ *
12
+ * That is a straight violation of the rule that the Python and JS clients
13
+ * behave identically: the Python client obviously ran, so the divergence looked
14
+ * like a model or endpoint problem rather than a missing binary.
15
+ *
16
+ * Kept in its own module, with the probe injected, so the resolution order is
17
+ * testable without spawning anything.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.PYTHON_CANDIDATES = exports.PYTHON_ENV_VAR = void 0;
21
+ exports.resolvePythonCommand = resolvePythonCommand;
22
+ /** Env var that overrides everything below it. */
23
+ exports.PYTHON_ENV_VAR = 'CAPTCHA_KRAKEN_PYTHON';
24
+ /**
25
+ * Candidates tried in order when nothing is configured.
26
+ *
27
+ * `python3` FIRST. On Debian-family systems `python` is absent; where both
28
+ * exist they are the same interpreter in practice, and PEP 394 makes `python3`
29
+ * the portable spelling. `python` stays as the fallback for Windows and for
30
+ * venvs that only expose the unsuffixed name.
31
+ */
32
+ exports.PYTHON_CANDIDATES = ['python3', 'python'];
33
+ /**
34
+ * Resolve the interpreter, most specific first:
35
+ *
36
+ * 1. an explicitly configured `pythonCommand`
37
+ * 2. `CAPTCHAKRAKEN_PYTHON`
38
+ * 3. the venv interpreter beside the bundled CLI
39
+ * 4. the first of `python3`, `python` that actually exists
40
+ *
41
+ * Falls back to `python3` rather than `python` when nothing can be probed: on
42
+ * the platform where the two differ, `python3` is the one that is present.
43
+ */
44
+ function resolvePythonCommand(options = {}) {
45
+ const { configured, venvPython, env = process.env, exists } = options;
46
+ if (configured)
47
+ return configured;
48
+ const fromEnv = env[exports.PYTHON_ENV_VAR];
49
+ if (fromEnv)
50
+ return fromEnv;
51
+ if (venvPython)
52
+ return venvPython;
53
+ if (exists) {
54
+ for (const candidate of exports.PYTHON_CANDIDATES) {
55
+ if (exists(candidate))
56
+ return candidate;
57
+ }
58
+ }
59
+ return exports.PYTHON_CANDIDATES[0];
60
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The algebra behind a puzzle-piece slider's closed loop.
3
+ *
4
+ * Split out of solver.ts because it is the one part of this feature that is
5
+ * pure arithmetic, duplicated across the two ports, and impossible to notice
6
+ * drifting: a wrong ratio here does not throw, it just releases the piece in the
7
+ * wrong place, and the puzzle fails the way an unsolved puzzle fails. Keeping it
8
+ * dependency-free means it can be unit-tested with `node --test` alongside
9
+ * limits.ts, against the same cases as
10
+ * `PageSolver._solve_slide_geometry` in the Python port.
11
+ */
12
+ /**
13
+ * Piece width and handle-to-piece travel ratio, from probe measurements.
14
+ *
15
+ * Each measurement is `[handle offset, width of what changed]`, where the width
16
+ * spans the piece's ORIGINAL left edge to its CURRENT right edge — so
17
+ * `width = pieceWidth + ratio x offset`. Two of them determine both unknowns.
18
+ *
19
+ * With only one usable measurement the system is underdetermined, so `ratio` is
20
+ * ASSUMED to be 1 — true of every vendor observed, and the assumption is stated
21
+ * here rather than buried as a default. A ratio solved from implausible
22
+ * measurements (a redraw, a piece that hit the wall between probes) is rejected
23
+ * the same way: better a 1:1 guess that overshoots and gets corrected than a
24
+ * ratio of 0.02 that sends the handle off the track and, under camoufox, into a
25
+ * mouse move that never returns.
26
+ *
27
+ * `pieceWidth: null` means "could not measure" — the caller falls back to
28
+ * steering by the handle's own travel.
29
+ */
30
+ export declare function solveSlideGeometry(widths: Array<[number, number]>, widgetWidth: number): {
31
+ pieceWidth: number | null;
32
+ ratio: number;
33
+ };
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * The algebra behind a puzzle-piece slider's closed loop.
4
+ *
5
+ * Split out of solver.ts because it is the one part of this feature that is
6
+ * pure arithmetic, duplicated across the two ports, and impossible to notice
7
+ * drifting: a wrong ratio here does not throw, it just releases the piece in the
8
+ * wrong place, and the puzzle fails the way an unsolved puzzle fails. Keeping it
9
+ * dependency-free means it can be unit-tested with `node --test` alongside
10
+ * limits.ts, against the same cases as
11
+ * `PageSolver._solve_slide_geometry` in the Python port.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.solveSlideGeometry = solveSlideGeometry;
15
+ /**
16
+ * Piece width and handle-to-piece travel ratio, from probe measurements.
17
+ *
18
+ * Each measurement is `[handle offset, width of what changed]`, where the width
19
+ * spans the piece's ORIGINAL left edge to its CURRENT right edge — so
20
+ * `width = pieceWidth + ratio x offset`. Two of them determine both unknowns.
21
+ *
22
+ * With only one usable measurement the system is underdetermined, so `ratio` is
23
+ * ASSUMED to be 1 — true of every vendor observed, and the assumption is stated
24
+ * here rather than buried as a default. A ratio solved from implausible
25
+ * measurements (a redraw, a piece that hit the wall between probes) is rejected
26
+ * the same way: better a 1:1 guess that overshoots and gets corrected than a
27
+ * ratio of 0.02 that sends the handle off the track and, under camoufox, into a
28
+ * mouse move that never returns.
29
+ *
30
+ * `pieceWidth: null` means "could not measure" — the caller falls back to
31
+ * steering by the handle's own travel.
32
+ */
33
+ function solveSlideGeometry(widths, widgetWidth) {
34
+ if (!widths.length)
35
+ return { pieceWidth: null, ratio: 1 };
36
+ let ratio = 1;
37
+ let pieceWidth = null;
38
+ if (widths.length >= 2) {
39
+ const [o1, w1] = widths[0];
40
+ const [o2, w2] = widths[widths.length - 1];
41
+ if (o2 !== o1) {
42
+ const candidate = (w2 - w1) / (o2 - o1);
43
+ if (candidate >= MIN_RATIO && candidate <= MAX_RATIO) {
44
+ ratio = candidate;
45
+ pieceWidth = w1 - ratio * o1;
46
+ }
47
+ }
48
+ }
49
+ if (pieceWidth === null) {
50
+ const [o, w] = widths[widths.length - 1];
51
+ pieceWidth = w - ratio * o;
52
+ }
53
+ // A piece narrower than a few pixels, or wider than half the widget, is a
54
+ // measurement of something else — a wholesale redraw, a fade, a spinner.
55
+ if (pieceWidth < MIN_PIECE_PX || pieceWidth > widgetWidth * MAX_PIECE_FRACTION) {
56
+ return { pieceWidth: null, ratio };
57
+ }
58
+ return { pieceWidth, ratio };
59
+ }
60
+ // Keep these identical to the literals in page_solver.py::_solve_slide_geometry.
61
+ const MIN_RATIO = 0.2;
62
+ const MAX_RATIO = 3.0;
63
+ const MIN_PIECE_PX = 3.0;
64
+ const MAX_PIECE_FRACTION = 0.6;
package/dist/solver.d.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  import { PlaywrightPage as Page, PlaywrightElementHandle as ElementHandle } from './playwright-types';
2
+ import { CaptchaWatcher, WatchOptions } from './watcher';
2
3
  import { CaptchaKrakenConfig, SolveResult } from './types';
4
+ /**
5
+ * The two numbers that bound a solve, together, because they only make sense
6
+ * together: `maxSolveLoops` is the count that FITS inside the timeout at the
7
+ * ~4-7s a round costs, and the timeout is the backstop for when it does not.
8
+ * Named rather than inlined as `??` defaults so the relationship between them
9
+ * can be pinned by a test — see no-progress.test.ts. The Python port keeps the
10
+ * same pair on `PageSolverConfig`.
11
+ */
12
+ export declare const SOLVE_DEFAULTS: {
13
+ readonly maxSolveLoops: 6;
14
+ readonly overallSolveTimeoutMs: 45000;
15
+ };
3
16
  export declare class CaptchaKrakenSolver {
4
17
  private config;
5
18
  private lastMousePosition;
@@ -15,11 +28,34 @@ export declare class CaptchaKrakenSolver {
15
28
  private cvWorkerPending;
16
29
  private cvWorkerBuf;
17
30
  private solutionCache;
31
+ private repeatedAnswerSeen;
32
+ private keyframeMode;
33
+ private lastAnswerSig;
34
+ private noProgressRounds;
18
35
  private state;
19
36
  private lastSubmitFrameHash;
20
37
  private solveSessionId;
21
38
  constructor(config?: CaptchaKrakenConfig);
22
39
  solve(page: Page): Promise<SolveResult | void>;
40
+ /**
41
+ * Install an auto-solver: probe `page` on a timer and solve any captcha that
42
+ * becomes visible, until `stop()`.
43
+ *
44
+ * Returns immediately — the watching happens in the background, so your
45
+ * automation carries on and captchas are handled underneath it.
46
+ *
47
+ * Nothing is injected into the page on any platform; see watcher.ts for why,
48
+ * and for the note on Camoufox, where the probe's DOM reads land in its
49
+ * isolated world by default.
50
+ *
51
+ * ```typescript
52
+ * const solver = new CaptchaKrakenSolver();
53
+ * const watcher = solver.watch(page);
54
+ * await page.goto('https://example.com/protected'); // solved as it appears
55
+ * await watcher.stop();
56
+ * ```
57
+ */
58
+ watch(page: Page, options?: WatchOptions): CaptchaWatcher;
23
59
  private solveImpl;
24
60
  /**
25
61
  * Fire the optional onStep observer with a fresh screenshot of the captcha
@@ -208,17 +244,37 @@ export declare class CaptchaKrakenSolver {
208
244
  /** Order a loading set so `priority` cells (just-clicked) come first. */
209
245
  private orderByPriority;
210
246
  /**
211
- * Detect whether any tiles are blank or fading, watching for the ONSET of the
212
- * reCAPTCHA refresh over a short grace window. The blank/fade transition lags
213
- * the click by a beat, so a single snapshot right after clicking misses it
214
- * (the tile still shows its old image — not yet white, not yet changing). We
215
- * poll consecutive frames and mark a cell loading if it is `empty` (≥97%
216
- * near-white) OR `changing` (>2% pixels differ). HOVERS a clicked tile each
217
- * poll so the mouse keeps moving (no unnatural pauses). Returns the loading
218
- * cells (priority/clicked first) as soon as any appears, or [] if the whole
219
- * window passes with nothing loading (→ solved). Logs every poll + frame.
247
+ * Watch the just-clicked tiles until the widget says what it did with them.
248
+ *
249
+ * Two answers because reCAPTCHA gives a click one of exactly two replies, and
250
+ * they are the two kinds of board:
251
+ *
252
+ * - `chipped`: the small blue chip landed in the tiles' top-left corners —
253
+ * the photos were KEPT. Nothing is on its way in, the selection is the
254
+ * answer, and the caller should press Verify.
255
+ * - `loading`: the photos are blanking or dissolving under a large centred
256
+ * check — those tiles are being SWAPPED, and what lands may match too, so
257
+ * the board has to be read again.
258
+ *
259
+ * A widget that swaps one clicked cell swaps them all, so the two never share
260
+ * a board and one look at the tiles we just clicked settles it. The chip is
261
+ * what the CV layer reports as `selected` (top-left corner only, behind a
262
+ * circularity and a centroid test a centred check fails), and we already read
263
+ * it on every poll. `chipped` needs EVERY watched tile: a partial reading is a
264
+ * misread, and calling a swapping board finished submits half an answer and
265
+ * burns the attempt, where calling a chipped board unfinished costs one
266
+ * inference.
267
+ *
268
+ * The blank/fade transition lags the click by a beat, so a single snapshot
269
+ * right after clicking misses it (the tile still shows its old image — not yet
270
+ * white, not yet changing). We poll consecutive frames and mark a cell loading
271
+ * if it is `empty` (≥97% near-white) OR `changing` (>2% pixels differ). HOVERS
272
+ * a clicked tile each poll so the mouse keeps moving (no unnatural pauses).
273
+ * Returns as soon as either verdict is in, or `{loading: [], chipped: false}`
274
+ * if the whole window passes with nothing happening (→ solved). Logs every
275
+ * poll + frame.
220
276
  */
221
- private currentLoadingCells;
277
+ private watchClickedTiles;
222
278
  /**
223
279
  * After loading is detected, wait until at least one of the given blank/fading
224
280
  * cells reaches the `loaded` state, HOVERING those cells (in order) the whole
@@ -241,13 +297,124 @@ export declare class CaptchaKrakenSolver {
241
297
  * and on a `wait`, it hovers the just-clicked / currently blank+fading tiles
242
298
  * (in click order) and waits for at least one to finish reloading before
243
299
  * re-screenshotting and re-solving — so we don't burn a solver call on a grid
244
- * that's still mid-fade. It submits only on `done`.
300
+ * that's still mid-fade.
301
+ *
302
+ * Only a board that SWAPS a clicked tile out is worth those extra rounds. One
303
+ * that ticks the tile and keeps the photo has been fully answered by the round
304
+ * that clicked it — same as the 4x4 — so `watchClickedTiles` reports the chip
305
+ * and this submits there and then. Rounds 2..N exist for the fading board and
306
+ * nothing else. It submits on `done`, and on a board that ticked our clicks.
245
307
  *
246
308
  * Returns the same shape as solveSingle so the outer solve loop — including the
247
309
  * under-selection retry and post-solve detectCaptcha — wraps it unchanged.
248
310
  */
249
311
  private solveRecaptchaGrid;
312
+ /**
313
+ * Record the animated challenge and return the directory holding the burst.
314
+ *
315
+ * The driver owns the browser, so it does the recording; the CLI does the
316
+ * slicing (`solve-animated`). One zero-padded PNG per frame, because the slicer
317
+ * sorts by name and reads the clip's temporal structure — `frame_9.png` sorting
318
+ * after `frame_10.png` would shuffle the burst and turn a detectable cycle into
319
+ * noise.
320
+ *
321
+ * Geometry comes from config and defaults to the collector's (4s @ 10fps), so a
322
+ * challenge recorded here is the same shape of artifact the model trained on.
323
+ * The caller must remove the directory when the actions are done with — the
324
+ * keyframes the wait gate re-reads on every poll live inside it.
325
+ */
326
+ private recordKeyframeBurst;
327
+ /**
328
+ * Slice a recorded burst into keyframes and solve them in ONE model request.
329
+ *
330
+ * Deliberately not routed through `solveFrameFreshnessGuarded`. That guard
331
+ * re-solves when the frame changes during inference, and an animated challenge
332
+ * changes by definition — every attempt would be judged stale and the whole
333
+ * re-solve budget would burn without ever acting. The `frame` in the answer is
334
+ * the real guard: it names the state to act in, and `waitForKeyframe` enforces it.
335
+ *
336
+ * Also not deduped by screenshot hash: there is no single screenshot, and two
337
+ * recordings of the same widget are never byte-identical anyway.
338
+ */
339
+ private getAnimatedSolution;
340
+ /**
341
+ * Hold until the widget looks like `keyframePath` around the 0–1 point (cx, cy).
342
+ *
343
+ * This is the reason an animated answer names a frame. The model picked the
344
+ * moment its target was visible, and the coordinates are only correct at that
345
+ * moment; clicking as soon as the answer arrives lands on whatever the sprite
346
+ * happens to be doing, which for a cross-fade is usually background.
347
+ *
348
+ * Only the neighbourhood of the action point is compared, with the same box and
349
+ * metric the training label's frame was chosen with. Local rather than
350
+ * whole-frame because everything ELSE in these puzzles is also moving: a
351
+ * whole-frame match would need every unrelated sprite to align too, and would
352
+ * essentially never open.
353
+ *
354
+ * Never throws. Returns whether the state was reached; on timeout the caller
355
+ * clicks anyway (see `keyframeWaitTimeoutMs`).
356
+ *
357
+ * NOT ATTEMPTED on an `even` clip. The slicer picks that mode precisely when
358
+ * the clip never revisits a picture it has already shown — a rotation, a
359
+ * one-way fade, a sprite crossing — so there is no state to come back to and
360
+ * this can only run out its full 6s, PER CLICK, before clicking the
361
+ * coordinates it already had. It is also the normal case, not a corner: all
362
+ * 116 real clips under cleanSamples/test/raw are `even` and `cycle` has never
363
+ * fired on real footage. Measured on hcaptcha_rotating_obj_video: 6.0s of a
364
+ * 28.8s solve, closest region diff 0.0721 against a 0.05 tolerance, then the
365
+ * same click, then solved. Kept for `cycle`/`static`, where the state does
366
+ * come back and waiting is the difference between the sprite and background.
367
+ */
368
+ private waitForKeyframe;
369
+ /**
370
+ * Per-solve state that must not leak into the next challenge on the page.
371
+ *
372
+ * A repeat is a fact about ONE challenge. Carrying it forward would make the
373
+ * captcha after a cycling one record a burst it does not need.
374
+ */
375
+ private resetSolveState;
376
+ /**
377
+ * A stable identity for "the answer this round is about to execute".
378
+ *
379
+ * Keyed on the retry mode too: the missed-tiles retry deliberately re-asks
380
+ * about the same board and its answer legitimately overlaps the previous one,
381
+ * so counting that as a repeat would abandon the one path built to recover
382
+ * from an under-selection.
383
+ *
384
+ * Coordinates are ROUNDED rather than compared exactly — the same tile chosen
385
+ * twice can differ in the last float digit after the normalise/clamp
386
+ * round-trip, and a repeat that reads as "different" is a repeat that costs a
387
+ * round. Returns null when the answer cannot be summarised, which counts as
388
+ * "not a repeat": an unreadable answer must never be why a solve is
389
+ * abandoned. Mirrors page_solver.py `_answer_signature`.
390
+ */
391
+ private static answerSignature;
392
+ /** Count consecutive identical answers; see `maxNoProgressRounds`. */
393
+ private noteAnswer;
394
+ /**
395
+ * The answer for this picture, asking the model only if we have not already.
396
+ *
397
+ * The cache saving is real and is kept: a byte-identical picture costs no
398
+ * second inference. What changed is what a hit MEANS. It used to mean
399
+ * "nothing has changed, so this answer still stands"; it actually means the
400
+ * answer already ran and moved nothing, because every answer this returns is
401
+ * executed. So the hit is served (it is free) and recorded, and the round
402
+ * after it stops guessing at a still frame — see `shouldRetryAsAnimated`.
403
+ */
404
+ private answerFor;
405
+ /**
406
+ * Should this round be recorded and solved from keyframes instead of read as
407
+ * a still?
408
+ *
409
+ * reCAPTCHA is excluded deliberately. Its dynamic 3x3 REPLACES tiles in place
410
+ * and has its own multi-round driver with its own fade gates; its grids are
411
+ * never animated, so escalating there would swap a path that works for one
412
+ * that cannot read a grid.
413
+ */
414
+ private shouldRetryAsAnimated;
250
415
  private getSolution;
416
+ /** One inference: build the CLI invocation, run it, parse what comes back. */
417
+ private askModel;
251
418
  /**
252
419
  * Query the model for `initialShot`, then guard against the captcha frame
253
420
  * having changed DURING inference. reCAPTCHA/hCaptcha fade fresh tiles in over
@@ -315,7 +482,7 @@ export declare class CaptchaKrakenSolver {
315
482
  /**
316
483
  * Run `fn` (typically the model query) while idly drifting the cursor over
317
484
  * the captcha, so the mouse behaves like a human weighing the options instead
318
- * of freezing during inference. Uses the same cursory-ts trajectories as real
485
+ * of freezing during inference. Uses the same generate_trajectory paths as real
319
486
  * clicks; cancelled the instant `fn` resolves. Best-effort — any wander error
320
487
  * is swallowed and never fails the solve. Disable via config.idleMouseWander.
321
488
  */
@@ -328,4 +495,39 @@ export declare class CaptchaKrakenSolver {
328
495
  private tracePath;
329
496
  private executeClick;
330
497
  private executeDrag;
498
+ /**
499
+ * First VISIBLE match for `selectors`, tried in order.
500
+ *
501
+ * `scope` is the challenge frame, or — for the vendors that render into the
502
+ * host page rather than an iframe — the widget element itself. Never the
503
+ * page: the generic tail of both selector tables would otherwise happily
504
+ * match a login form's text box or a carousel's drag handle somewhere else on
505
+ * the document, and the answer would go there.
506
+ */
507
+ private findControl;
508
+ /** Put the model's reading of a distorted-text captcha into its box. */
509
+ private executeType;
510
+ /** `captchakraken track-piece` — box of what moved, handle masked out. */
511
+ private trackPiece;
512
+ /**
513
+ * Drive a puzzle-piece slider until the PIECE reaches the model's slot.
514
+ *
515
+ * The model is asked for one thing here — the centre of the gap — because it
516
+ * is the only thing the picture can tell it. What it cannot know is how far
517
+ * the handle must travel to put the piece there: the handle is elsewhere on
518
+ * the widget, and the ratio between the two is a vendor implementation detail
519
+ * that several of them deliberately vary.
520
+ *
521
+ * So this is closed-loop, not a calculation. Press the handle, nudge it twice
522
+ * by known amounts, and watch the screen: union(before, after) spans the
523
+ * piece's ORIGINAL left edge to its CURRENT right edge, so its width is
524
+ * pieceWidth + ratio x nudge. Two nudges, two widths, two unknowns — solve for
525
+ * both, then steer the remaining distance and re-measure. The mouse is not
526
+ * released until the piece is home, because on every one of these puzzles
527
+ * releasing IS the submit; there is no Verify button to reconsider at.
528
+ *
529
+ * Returns false if there is nothing here to drag, leaving the caller's normal
530
+ * no-op handling to deal with it.
531
+ */
532
+ private executeSlide;
331
533
  }