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
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PIECE_SELECTORS = exports.SLIDER_HANDLE_SELECTORS = exports.TEXT_INPUT_SELECTORS = exports.TEXT_INPUT_VENDOR_SELECTORS = exports.SUBMIT_SELECTORS = exports.submitByText = exports.ACCEPTED_SELECTORS = exports.RESPONSE_SELECTORS = exports.WIDGET_PROBES = exports.VENDORS = exports.SELECTORS = void 0;
4
+ /**
5
+ * Every selector the driver knows, keyed by vendor. Mirrored in python/src/captchakraken/selectors.py; keep both in the
6
+ * same order, because detection takes the first visible match in table order.
7
+ */
8
+ const kinds_1 = require("./kinds");
9
+ exports.SELECTORS = {
10
+ [kinds_1.Vendor.RECAPTCHA]: {
11
+ hosts: ['google.com/recaptcha', 'recaptcha.net'],
12
+ challenge: ['iframe[src*="recaptcha/api2/bframe"]'],
13
+ checkbox: ['iframe[src*="recaptcha/api2/anchor"]:not([src*="size=invisible"])'],
14
+ response: '[name="g-recaptcha-response"]',
15
+ checked: '.recaptcha-checkbox-checked',
16
+ fresh: '.rc-imageselect-instructions, #rc-imageselect',
17
+ submit: ['#recaptcha-verify-button'],
18
+ banners: [
19
+ ['.rc-imageselect-error-select-more', kinds_1.RecaptchaBanner.SELECT_MORE],
20
+ ['.rc-imageselect-error-dynamic-more', kinds_1.RecaptchaBanner.DYNAMIC_MORE],
21
+ ['.rc-imageselect-incorrect-response', kinds_1.RecaptchaBanner.REJECTED],
22
+ ],
23
+ },
24
+ // Keyed on the `hcaptcha` substring, not the apex host: challenges are served off newassets.hcaptcha.com.
25
+ [kinds_1.Vendor.HCAPTCHA]: {
26
+ hosts: ['hcaptcha.com'],
27
+ challenge: ['iframe[src*="hcaptcha"][src*="frame=challenge"]'],
28
+ checkbox: ['iframe[src*="hcaptcha"][src*="frame=checkbox"]'],
29
+ response: '[name="h-captcha-response"]',
30
+ checked: '#checkbox[aria-checked="true"]',
31
+ fresh: '.prompt-text',
32
+ images: ['.task-image .image', '.task .image', '.challenge-example img', '.image-wrapper img'],
33
+ submit: ['.button-submit'],
34
+ },
35
+ [kinds_1.Vendor.TURNSTILE]: {
36
+ hosts: ['challenges.cloudflare.com'],
37
+ checkbox: ['iframe[src*="challenges.cloudflare.com"]', '.cf-turnstile'],
38
+ response: '[name="cf-turnstile-response"]',
39
+ },
40
+ [kinds_1.Vendor.GEETEST]: {
41
+ hosts: ['geetest.com'],
42
+ widget: ['.geetest_box', '.geetest_panel_box', '.geetest_popup_window', '.geetest_widget'],
43
+ accepted: '.geetest_result_tips.geetest_success, .geetest_captcha.geetest_success, .geetest_captcha.geetest_lock_success',
44
+ submit: ['.geetest_submit'],
45
+ sliderHandle: ['.geetest_slider_button', '.geetest_btn', '.geetest_slider .geetest_arrow'],
46
+ piece: ['.geetest_slice'],
47
+ },
48
+ // Both in-page and iframe shapes stay (Tencent moved in-host on 2026-08-11; see TRIBAL_KNOWLEDGE.md), and the iframe id
49
+ // is prefix-anchored: `[id*=tcaptcha]` also matched MTCaptcha's iframe and hid that `.mtcap` matched nothing.
50
+ [kinds_1.Vendor.TENCENT]: {
51
+ hosts: ['captcha.gtimg.com', 'captcha.qcloud.com'],
52
+ widget: ['#tcaptcha_transform_dy', '#tCaptchaDyContent', '.tencent-captcha-dy__content', 'iframe#tcaptcha_iframe_dy', 'iframe[id^="tcaptcha"]', 'iframe[src*="captcha.gtimg.com"]', 'iframe[src*="captcha.qq.com"]'],
53
+ sliderHandle: ['.tencent-captcha-dy__slider-block', '#tcaptcha_drag_thumb', '.tc-slider-normal', '[id*=slideBlock]'],
54
+ piece: ['.tencent-captcha-dy__fg-item'],
55
+ },
56
+ [kinds_1.Vendor.YIDUN]: {
57
+ hosts: ['dun.163.com', 'cstaticdun.126.net', 'necaptcha.nosdn.127.net'],
58
+ widget: ['.yidun_panel', '.yidun'],
59
+ sliderHandle: ['.yidun_slider', '.yidun_jigsaw'],
60
+ piece: ['.yidun_jigsaw'],
61
+ },
62
+ [kinds_1.Vendor.YANDEX]: {
63
+ hosts: ['smartcaptcha.yandexcloud.net'],
64
+ widget: ['iframe[src*="smartcaptcha.yandexcloud.net/advanced"]', 'iframe[src*="smartcaptcha.yandexcloud.net"]', '.CheckboxCaptcha'],
65
+ textInput: ['.AdvancedCaptcha-Input input', 'input.Textinput-Control', 'input[name="rep"]'],
66
+ },
67
+ [kinds_1.Vendor.LEMIN]: {
68
+ hosts: ['leminnow.com'],
69
+ widget: ['#lemin-cropped-captcha', '.lemin-captcha-popup'],
70
+ sliderHandle: ['.lemin-slider-handle', '#lemin-cropped-captcha .slider'],
71
+ piece: ['.lemin-cropped-puzzle-piece', '#lemin-cropped-captcha canvas + canvas'],
72
+ },
73
+ [kinds_1.Vendor.PROSOPO]: {
74
+ hosts: ['prosopo.io'],
75
+ widget: ['.prosopo-modalInner', '.procaptcha-checkbox'],
76
+ },
77
+ [kinds_1.Vendor.MTCAPTCHA]: {
78
+ hosts: ['mtcaptcha.com'],
79
+ widget: ['iframe[src*="service.mtcaptcha.com"]', 'iframe[id^="mtcaptcha-iframe"]', '.mtcaptcha', '.mtcap'],
80
+ textInput: ['input.mtcap-inputtext', '.mtcap input[type=text]'],
81
+ },
82
+ [kinds_1.Vendor.BOTDETECT]: {
83
+ hosts: [],
84
+ widget: ['.BDC_CaptchaDiv'],
85
+ textInput: ['input[id*=captchaCode]', 'input#captchaCode', 'input[id*=validateCaptcha]', '.BDC_CaptchaDiv input[type=text]'],
86
+ },
87
+ [kinds_1.Vendor.UNKNOWN]: { hosts: [] },
88
+ };
89
+ exports.VENDORS = Object.entries(exports.SELECTORS);
90
+ const ofEveryVendor = (key) => exports.VENDORS.flatMap(([, s]) => s[key] ?? []);
91
+ /** Every host-page selector with what it names, in detection order: open challenges, then checkboxes, then inline widgets. */
92
+ exports.WIDGET_PROBES = [
93
+ [kinds_1.FrameRole.CHALLENGE, 'challenge'], [kinds_1.FrameRole.CHECKBOX, 'checkbox'], [kinds_1.FrameRole.UNKNOWN, 'widget'],
94
+ ].flatMap(([role, key]) => exports.VENDORS.flatMap(([vendor, s]) => (s[key] ?? []).map((selector) => ({ vendor, role, selector }))));
95
+ exports.RESPONSE_SELECTORS = exports.VENDORS.flatMap(([, s]) => s.response ?? []);
96
+ exports.ACCEPTED_SELECTORS = exports.VENDORS.flatMap(([, s]) => s.accepted ?? []);
97
+ const lower = "translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')";
98
+ /** `.//` keeps the xpath relative so a host-page widget cannot reach the form's own submit. */
99
+ const submitByText = (text) => `xpath=.//button[contains(${lower}, '${text}')] | .//div[@role="button" and contains(${lower}, '${text}')]`;
100
+ exports.submitByText = submitByText;
101
+ /** A real Verify/Next/Submit/Skip first; the vendor-named controls are fallbacks. */
102
+ exports.SUBMIT_SELECTORS = [...['verify', 'next', 'submit', 'skip'].map(exports.submitByText), ...ofEveryVendor('submit')];
103
+ // Vendor-named first, generic last: the driver takes the first visible match, and a generic selector
104
+ // reached before the vendor's own is how a captcha's answer ends up in a login form's username box.
105
+ exports.TEXT_INPUT_VENDOR_SELECTORS = ofEveryVendor('textInput');
106
+ exports.TEXT_INPUT_SELECTORS = [...exports.TEXT_INPUT_VENDOR_SELECTORS,
107
+ 'input[name*="captcha" i]', 'input[id*="captcha" i]', 'input[aria-label*="captcha" i]',
108
+ 'input[placeholder*="code" i]', 'input[autocomplete="off"][type=text]',
109
+ 'input[type=text]', 'input:not([type])', 'input[type=tel]', 'textarea',
110
+ ];
111
+ // `[draggable=true]` is absent: HTML5 DnD fires dragstart, not pointermove.
112
+ exports.SLIDER_HANDLE_SELECTORS = [...ofEveryVendor('sliderHandle'),
113
+ '[role="slider"]', '[aria-valuenow]',
114
+ '[class*="slider"][class*="btn"]', '[class*="slider"][class*="button"]',
115
+ '[class*="slide"][class*="handle"]', '[class*="drag"][class*="thumb"]',
116
+ ];
117
+ exports.PIECE_SELECTORS = [...ofEveryVendor('piece'), '[class*="puzzle"][class*="piece"]', '[class*="jigsaw"]'];
@@ -1,33 +1,6 @@
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
1
  export declare function solveSlideGeometry(widths: Array<[number, number]>, widgetWidth: number): {
31
2
  pieceWidth: number | null;
32
3
  ratio: number;
33
4
  };
5
+ export declare const MIN_PIECE_PX = 3;
6
+ export declare const MAX_PIECE_FRACTION = 0.6;
@@ -1,64 +1,38 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_PIECE_FRACTION = exports.MIN_PIECE_PX = void 0;
14
4
  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
- */
5
+ // Literals here match page_solver.py::_solve_slide_geometry; the ratio is assumed 1:1 unless two probes prove otherwise,
6
+ // which is true of every vendor observed.
33
7
  function solveSlideGeometry(widths, widgetWidth) {
34
8
  if (!widths.length)
35
9
  return { pieceWidth: null, ratio: 1 };
36
10
  let ratio = 1;
37
11
  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
- }
12
+ // The widest-apart pair, not the last two: 1px rounding between neighbouring probes once gave a ratio of 0.5.
13
+ const sorted = [...widths].sort((a, b) => a[0] - b[0]);
14
+ const [o1, w1] = sorted[0];
15
+ const [o2, w2] = sorted[sorted.length - 1];
16
+ if (o2 - o1 >= MIN_OFFSET_SPREAD_PX) {
17
+ const candidate = (w2 - w1) / (o2 - o1);
18
+ if (candidate >= MIN_RATIO && candidate <= MAX_RATIO) {
19
+ ratio = candidate;
20
+ pieceWidth = w1 - ratio * o1;
47
21
  }
48
22
  }
49
23
  if (pieceWidth === null) {
50
24
  const [o, w] = widths[widths.length - 1];
51
25
  pieceWidth = w - ratio * o;
52
26
  }
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) {
27
+ if (pieceWidth < exports.MIN_PIECE_PX || pieceWidth > widgetWidth * exports.MAX_PIECE_FRACTION) {
56
28
  return { pieceWidth: null, ratio };
57
29
  }
58
30
  return { pieceWidth, ratio };
59
31
  }
60
- // Keep these identical to the literals in page_solver.py::_solve_slide_geometry.
32
+ // Box widths are ±1px, so the ratio error is ±2/spread: ±0.13 at 16px against ±0.5 at 4px, multiplied into every correction.
33
+ const MIN_OFFSET_SPREAD_PX = 16.0;
34
+ // A ratio of 0.02 from a redraw sends the handle off the track and, under camoufox, into a mouse move that never returns.
61
35
  const MIN_RATIO = 0.2;
62
36
  const MAX_RATIO = 3.0;
63
- const MIN_PIECE_PX = 3.0;
64
- const MAX_PIECE_FRACTION = 0.6;
37
+ exports.MIN_PIECE_PX = 3.0;
38
+ exports.MAX_PIECE_FRACTION = 0.6;