captchakraken 3.0.0 → 3.2.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 (44) hide show
  1. package/AGENTS.md +4 -4
  2. package/LICENSE +1 -1
  3. package/README.md +10 -13
  4. package/dist/index.d.ts +1 -0
  5. package/dist/kinds.d.ts +8 -0
  6. package/dist/kinds.js +3 -1
  7. package/dist/model-name.d.ts +6 -0
  8. package/dist/model-name.js +28 -0
  9. package/dist/playwright-types.d.ts +18 -0
  10. package/dist/png.d.ts +16 -0
  11. package/dist/png.js +119 -0
  12. package/dist/puppeteer-adapter.d.ts +7 -1
  13. package/dist/puppeteer-adapter.js +6 -0
  14. package/dist/selectors.d.ts +6 -0
  15. package/dist/selectors.js +4 -0
  16. package/dist/solver.d.ts +118 -5
  17. package/dist/solver.js +840 -143
  18. package/dist/types.d.ts +12 -5
  19. package/dist/verdicts.d.ts +45 -0
  20. package/dist/verdicts.js +125 -0
  21. package/package.json +7 -7
  22. package/python/AGENTS.md +13 -8
  23. package/python/LICENSE +1 -1
  24. package/python/README.md +10 -13
  25. package/python/pyproject.toml +6 -6
  26. package/python/src/captchakraken/__init__.py +5 -1
  27. package/python/src/captchakraken/cli.py +7 -1
  28. package/python/src/captchakraken/kinds.py +9 -0
  29. package/python/src/captchakraken/page_solver.py +651 -137
  30. package/python/src/captchakraken/planner.py +68 -12
  31. package/python/src/captchakraken/prompts.py +24 -0
  32. package/python/src/captchakraken/selectors.py +10 -0
  33. package/python/src/captchakraken/tool_calls/find_checkbox.py +4 -0
  34. package/python/src/captchakraken/tool_calls/find_grid.py +22 -0
  35. package/python/src/captchakraken/verdicts.py +154 -0
  36. package/python/Dockerfile +0 -41
  37. package/python/examples/README.md +0 -114
  38. package/python/examples/_harness.py +0 -177
  39. package/python/examples/demoHcaptcha.py +0 -8
  40. package/python/examples/demoRecaptcha.py +0 -8
  41. package/python/examples/watch_playwright.py +0 -31
  42. package/python/examples/with_local_gguf.py +0 -28
  43. package/python/examples/with_playwright.py +0 -24
  44. package/scripts/copy-python.mjs +0 -29
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { RoundVerdict } from './verdicts';
1
2
  import type { Humanizer, TouchTransform } from './humanize.js';
2
3
  import type { ActionKind, FrameRole, HumanizationMode, PromptFamily, SolveStage, Vendor } from './kinds';
3
4
  /** One intermediate stage of a solve, handed to `CaptchaKrakenConfig.onStep`. */
@@ -43,9 +44,11 @@ export interface CaptchaKrakenConfig {
43
44
  };
44
45
  /** Solve rounds that fit the timeout. Default 6. */
45
46
  maxSolveLoops?: number;
46
- /** Consecutive identical answers before the solve is abandoned. Default 2. */
47
+ /** How long to keep looking for a widget the page has not drawn yet, before "no captcha" is believed. Not charged to `overallSolveTimeoutMs`. Default 15000. */
48
+ detectionTimeoutMs?: number;
49
+ /** Repeats of one answer on one board before the solve ends; the first repeats are re-asked at a fresh sample. Default 2. */
47
50
  maxNoProgressRounds?: number;
48
- /** Dwell after a round that answered nothing. Default 1200. */
51
+ /** @deprecated Ignored since 3.2.0: a round that performed nothing goes straight on to the next. */
49
52
  postSolveDelayMs?: number;
50
53
  /** Whole-solve time limit. Default 45000. */
51
54
  overallSolveTimeoutMs?: number;
@@ -105,6 +108,8 @@ export interface CaptchaKrakenConfig {
105
108
  keyframeWaitPollMs?: number;
106
109
  /** Burst ceiling for a board that never repeats a screen. Default 12000. */
107
110
  videoBurstMaxMs?: number;
111
+ /** Safety ceiling on the camera that films an animated board across rounds. Default 120000: it is meant to end with the solve, not on this clock. */
112
+ videoFilmMaxMs?: number;
108
113
  /** Consecutive moved settle polls that mean animated. Default 5. */
109
114
  animatedMotionStreak?: number;
110
115
  /** Record while the still is being read, so a cycling board is known before its answer runs. Default true. */
@@ -117,11 +122,11 @@ export interface CaptchaKrakenConfig {
117
122
  elementScreenshotTimeoutMs?: number;
118
123
  /** Timeout for an `onStep` screenshot. Default 2000. */
119
124
  stepScreenshotTimeoutMs?: number;
120
- /** Re-detect retries after a stale handle. Default 3. */
125
+ /** @deprecated Ignored since 3.2.0: a stale handle now counts against `maxSolveLoops`. */
121
126
  maxStaleElementRetries?: number;
122
- /** Backoff before re-detecting. Default 900. */
127
+ /** The pause before a failed round is tried again. Default 900. */
123
128
  staleElementBackoffMs?: number;
124
- /** Retries when "unsupported" arrives mid-solve on a transitional frame. Default 3. */
129
+ /** @deprecated Ignored since 3.2.0: an unusable answer now counts against `maxSolveLoops`. */
125
130
  maxUnsupportedReSolves?: number;
126
131
  }
127
132
  export interface BoundingBox {
@@ -188,4 +193,6 @@ export interface SolveResult {
188
193
  };
189
194
  /** Milliseconds per phase, plus `total`. */
190
195
  phases?: Record<string, number>;
196
+ /** What the vendor's own server answered, in order, for the vendors whose answer is readable. */
197
+ verdicts: RoundVerdict[];
191
198
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The vendor's own answer to each submitted round, read off the wire. Mirrors python/src/captchakraken/verdicts.py.
3
+ *
4
+ * Every shape below was recorded against the vendor's public demo page, never inferred: a DOM done-signal can only
5
+ * say "something changed", while the answer-check response says whether the round was taken. A vendor with no
6
+ * readable answer (Turnstile, and the rest of the table) is judged by the DOM signals in the driver instead.
7
+ */
8
+ import { Verdict, Vendor } from './kinds';
9
+ import type { PlaywrightPage } from './playwright-types';
10
+ export interface RoundVerdict {
11
+ vendor: Vendor;
12
+ verdict: Verdict;
13
+ }
14
+ export interface VerdictEndpoint {
15
+ /** A URL substring naming the vendor's own endpoint. */
16
+ marker: string;
17
+ read: (body: string) => Verdict | null;
18
+ }
19
+ /** Only a vendor listed here has a readable answer; the rest are judged by the DOM done-signals. */
20
+ export declare const VERDICT_ENDPOINTS: Partial<Record<Vendor, readonly VerdictEndpoint[]>>;
21
+ /** The vendor and endpoint whose marker the URL carries, so a listener reads only the bodies it can judge. */
22
+ export declare function endpointFor(url: string): {
23
+ vendor: Vendor;
24
+ endpoint: VerdictEndpoint;
25
+ } | null;
26
+ export declare function readVerdict(url: string, status: number, body: string): RoundVerdict | null;
27
+ /**
28
+ * Every verdict the page's network carries while a solve runs.
29
+ *
30
+ * A page-level listener sees the vendors' cross-origin frames too. It is optional: a page object with no `on`
31
+ * records nothing, and the driver then judges every round by the DOM, as it always has.
32
+ */
33
+ export declare class VerdictLog {
34
+ private readonly page;
35
+ readonly verdicts: RoundVerdict[];
36
+ private read;
37
+ private pending;
38
+ private readonly listener;
39
+ constructor(page: PlaywrightPage);
40
+ /** The verdicts that arrived since the last call, once every body already on the wire has been read. */
41
+ fresh(): Promise<RoundVerdict[]>;
42
+ /** The last accept or reject, which is what the round came to. */
43
+ decisive(): Verdict | null;
44
+ close(): Promise<void>;
45
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VerdictLog = exports.VERDICT_ENDPOINTS = void 0;
4
+ exports.endpointFor = endpointFor;
5
+ exports.readVerdict = readVerdict;
6
+ /**
7
+ * The vendor's own answer to each submitted round, read off the wire. Mirrors python/src/captchakraken/verdicts.py.
8
+ *
9
+ * Every shape below was recorded against the vendor's public demo page, never inferred: a DOM done-signal can only
10
+ * say "something changed", while the answer-check response says whether the round was taken. A vendor with no
11
+ * readable answer (Turnstile, and the rest of the table) is judged by the DOM signals in the driver instead.
12
+ */
13
+ const kinds_1 = require("./kinds");
14
+ // The vendors' own HTTP refusal: hCaptcha's frame words a 429 as "Your computer or network has sent too many requests".
15
+ const TOO_MANY_REQUESTS = 429;
16
+ const parse = (body) => {
17
+ try {
18
+ return JSON.parse(body);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ };
24
+ /** A JSON object whose boolean `pass` is the verdict. */
25
+ const passField = (body) => {
26
+ const passed = parse(body)?.pass;
27
+ return typeof passed !== 'boolean' ? null : passed ? kinds_1.Verdict.ACCEPTED : kinds_1.Verdict.REJECTED;
28
+ };
29
+ /** A JSON array behind the `)]}'` anti-hijacking line. */
30
+ const xssiArray = (body) => {
31
+ const data = parse(body.startsWith(")]}'") && body.includes('\n') ? body.slice(body.indexOf('\n') + 1) : body);
32
+ return Array.isArray(data) && data.length ? data : null;
33
+ };
34
+ /** `["uvresp", token, 1, lifetime, ...]` when taken; `["uvresp", context, 0, ..., ["rresp", ...next board]]` when refused. */
35
+ const userVerify = (body) => {
36
+ const data = xssiArray(body);
37
+ if (!data || data[0] !== 'uvresp' || data.length < 3)
38
+ return null;
39
+ return data[2] === 1 ? kinds_1.Verdict.ACCEPTED : data[2] === 0 ? kinds_1.Verdict.REJECTED : null;
40
+ };
41
+ const newBoard = (body) => (xssiArray(body)?.[0] === 'rresp' ? kinds_1.Verdict.NEW_CHALLENGE : null);
42
+ /** `callback({...})` whose `data.result` is the verdict. */
43
+ const jsonpResult = (body) => {
44
+ const start = body.indexOf('(');
45
+ const end = body.lastIndexOf(')');
46
+ const data = (start >= 0 && start < end ? parse(body.slice(start + 1, end)) : null);
47
+ const result = data?.status === 'success' ? data.data?.result : undefined;
48
+ return result === 'success' ? kinds_1.Verdict.ACCEPTED : result === 'fail' ? kinds_1.Verdict.REJECTED : null;
49
+ };
50
+ const urlOnly = () => kinds_1.Verdict.NEW_CHALLENGE;
51
+ /** Only a vendor listed here has a readable answer; the rest are judged by the DOM done-signals. */
52
+ exports.VERDICT_ENDPOINTS = {
53
+ // The deal endpoint's body is encrypted, so its URL is the whole signal.
54
+ [kinds_1.Vendor.HCAPTCHA]: [
55
+ { marker: 'hcaptcha.com/checkcaptcha/', read: passField },
56
+ { marker: 'hcaptcha.com/getcaptcha/', read: urlOnly },
57
+ ],
58
+ [kinds_1.Vendor.RECAPTCHA]: [
59
+ { marker: '/recaptcha/api2/userverify', read: userVerify },
60
+ { marker: '/recaptcha/api2/reload', read: newBoard },
61
+ ],
62
+ [kinds_1.Vendor.GEETEST]: [
63
+ { marker: 'geetest.com/verify', read: jsonpResult },
64
+ { marker: 'geetest.com/load', read: urlOnly },
65
+ ],
66
+ };
67
+ /** The vendor and endpoint whose marker the URL carries, so a listener reads only the bodies it can judge. */
68
+ function endpointFor(url) {
69
+ return Object.entries(exports.VERDICT_ENDPOINTS)
70
+ .flatMap(([vendor, endpoints]) => endpoints.map((endpoint) => ({ vendor, endpoint })))
71
+ .find(({ endpoint }) => url.includes(endpoint.marker)) ?? null;
72
+ }
73
+ function readVerdict(url, status, body) {
74
+ const found = endpointFor(url);
75
+ if (!found)
76
+ return null;
77
+ if (status === TOO_MANY_REQUESTS)
78
+ return { vendor: found.vendor, verdict: kinds_1.Verdict.BLOCKED };
79
+ const verdict = status >= 200 && status < 300 ? found.endpoint.read(body) : null;
80
+ return verdict ? { vendor: found.vendor, verdict } : null;
81
+ }
82
+ /**
83
+ * Every verdict the page's network carries while a solve runs.
84
+ *
85
+ * A page-level listener sees the vendors' cross-origin frames too. It is optional: a page object with no `on`
86
+ * records nothing, and the driver then judges every round by the DOM, as it always has.
87
+ */
88
+ class VerdictLog {
89
+ constructor(page) {
90
+ this.page = page;
91
+ this.verdicts = [];
92
+ this.read = 0;
93
+ this.pending = [];
94
+ this.listener = (response) => {
95
+ const url = response.url();
96
+ const status = response.status();
97
+ if (!endpointFor(url))
98
+ return;
99
+ const body = status === TOO_MANY_REQUESTS ? Promise.resolve('') : response.text();
100
+ this.pending.push(body.then((text) => {
101
+ const verdict = readVerdict(url, status, text);
102
+ if (verdict)
103
+ this.verdicts.push(verdict);
104
+ }).catch(() => { }));
105
+ };
106
+ page.on?.('response', this.listener);
107
+ }
108
+ /** The verdicts that arrived since the last call, once every body already on the wire has been read. */
109
+ async fresh() {
110
+ await Promise.all(this.pending.splice(0));
111
+ const fresh = this.verdicts.slice(this.read);
112
+ this.read = this.verdicts.length;
113
+ return fresh;
114
+ }
115
+ /** The last accept or reject, which is what the round came to. */
116
+ decisive() {
117
+ return [...this.verdicts].reverse().map((v) => v.verdict)
118
+ .find((v) => v === kinds_1.Verdict.ACCEPTED || v === kinds_1.Verdict.REJECTED) ?? null;
119
+ }
120
+ async close() {
121
+ this.page.off?.('response', this.listener);
122
+ await Promise.all(this.pending.splice(0));
123
+ }
124
+ }
125
+ exports.VerdictLog = VerdictLog;
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "captchakraken",
3
- "version": "3.0.0",
4
- "description": "Captcha solver for Playwright and Puppeteer. Finds the captcha, reads the puzzle with a fine-tuned Qwen3.5-9B vision model, and clicks, drags, slides or types through to a token \u2014 reCAPTCHA, hCaptcha, GeeTest, NetEase Yidun, Tencent, Lemin, Prosopo, Turnstile and distorted text. Hosted API or self-hosted.",
3
+ "version": "3.2.0",
4
+ "description": "Captcha solver for Playwright and Puppeteer. Finds the captcha, reads the puzzle with a fine-tuned Qwen3.5-9B vision model, and clicks, drags, slides or types through to a token \u2014 reCAPTCHA, GeeTest, NetEase Yidun, Tencent, Lemin, Prosopo, Turnstile and distorted text. Hosted API or self-hosted.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist",
9
9
  "NOTICE",
10
10
  "python",
11
- "scripts",
11
+ "!python/**/__pycache__",
12
+ "scripts/setup-python.js",
12
13
  "AGENTS.md",
13
14
  "LICENSE"
14
15
  ],
@@ -24,14 +25,13 @@
24
25
  },
25
26
  "repository": {
26
27
  "type": "git",
27
- "url": "git+https://github.com/JWriter20/CaptchaKraken.git",
28
+ "url": "git+https://github.com/CaptchaKraken/CaptchaKraken.git",
28
29
  "directory": "js"
29
30
  },
30
31
  "keywords": [
31
32
  "captcha",
32
33
  "captcha-solver",
33
34
  "recaptcha",
34
- "hcaptcha",
35
35
  "geetest",
36
36
  "turnstile",
37
37
  "yidun",
@@ -49,9 +49,9 @@
49
49
  "license": "SEE LICENSE IN LICENSE",
50
50
  "type": "commonjs",
51
51
  "bugs": {
52
- "url": "https://github.com/JWriter20/CaptchaKraken/issues"
52
+ "url": "https://github.com/CaptchaKraken/CaptchaKraken/issues"
53
53
  },
54
- "homepage": "https://github.com/JWriter20/CaptchaKraken#readme",
54
+ "homepage": "https://github.com/CaptchaKraken/CaptchaKraken#readme",
55
55
  "dependencies": {
56
56
  "cursory-js": "2.0.1"
57
57
  },
package/python/AGENTS.md CHANGED
@@ -9,7 +9,7 @@ browser — bring your own — and every inference goes to a model endpoint you
9
9
  point it at. The `serve` extra is what turns this box into that endpoint.
10
10
 
11
11
  Repo, guides and the full agent guide:
12
- <https://github.com/JWriter20/CaptchaKraken>
12
+ <https://github.com/CaptchaKraken/CaptchaKraken>
13
13
 
14
14
  ---
15
15
 
@@ -71,10 +71,15 @@ module docstring of `captchakraken/cli.py` lists every mode with its arguments.
71
71
  ## 4. Errors
72
72
 
73
73
  `PageSolver.solve()` raises from `captchakraken.page_solver`:
74
- `NoCaptchaFoundError` (no interactive widget — usually not a failure),
75
- `UnsupportedChallengeError`, `AnimatedChallengeError` (the challenge could not
76
- be *recorded*), `PageClosedError`, and `CaptchaSolveError`, which is the base
77
- class of the other four — catch it last or it swallows them. The TypeScript
74
+ `NoCaptchaFoundError` (no interactive widget appeared within
75
+ `detection_timeout_ms` — usually not a failure), `VendorBlockedError` (the
76
+ vendor refused to serve this client at all), `UnsupportedChallengeError`,
77
+ `AnimatedChallengeError` (the challenge could not be *recorded*),
78
+ `PageClosedError`, and `CaptchaSolveError`, which is the base class of the
79
+ other five — catch it last or it swallows them. Apart from a vendor refusal, a
80
+ closed page and the time budget, a solve only gives up once every one of
81
+ `max_solve_loops` has been spent. `result.verdicts` holds the vendor's own
82
+ answer to each round where its answer-check response is readable. The TypeScript
78
83
  port raises none of these; it exports one error class and throws plain `Error`
79
84
  otherwise.
80
85
 
@@ -100,7 +105,7 @@ generation and the weights drift apart.
100
105
 
101
106
  - **Never invent a number.** Every accuracy, latency and price figure we publish
102
107
  is on one page —
103
- [docs/benchmarks.md](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/benchmarks.md).
108
+ [docs/benchmarks.md](https://github.com/CaptchaKraken/CaptchaKraken/blob/main/docs/benchmarks.md).
104
109
  Quote it as written; do not derive, round, or convert anything, and do not
105
110
  read a static-image rate as a promise about a browser run.
106
111
  - **Never print an API key** into a transcript, a log, or a commit. Use
@@ -116,7 +121,7 @@ generation and the weights drift apart.
116
121
  - **Respect the licence.** Building automation with this is fine; selling the
117
122
  solve, or shipping it inside a browser or automation product, is not. See
118
123
  [LICENSE](./LICENSE) and
119
- [docs/licensing.md](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/licensing.md).
124
+ [docs/licensing.md](https://github.com/CaptchaKraken/CaptchaKraken/blob/main/docs/licensing.md).
120
125
  - **Changing this package's own code?** Its rules are in
121
- [CONTRIBUTING.md](https://github.com/JWriter20/CaptchaKraken/blob/main/CONTRIBUTING.md),
126
+ [CONTRIBUTING.md](https://github.com/CaptchaKraken/CaptchaKraken/blob/main/CONTRIBUTING.md),
122
127
  not here.
package/python/LICENSE CHANGED
@@ -141,5 +141,5 @@ This text is a license, not legal advice. If the boundary between a permitted
141
141
  "value-add" product and a prohibited "thin wrapper", or between using the
142
142
  Software with a browser and shipping it as part of one, is unclear for your use
143
143
  case, ask by opening an issue or messaging the maintainer on GitHub:
144
- https://github.com/JWriter20/CaptchaKraken
144
+ https://github.com/CaptchaKraken/CaptchaKraken
145
145
  --------------------------------------------------------------------------------
package/python/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/logo-card.png" alt="CaptchaKraken" width="128" height="128">
2
+ <img src="https://raw.githubusercontent.com/CaptchaKraken/CaptchaKraken/main/docs/assets/logo-card.png" alt="CaptchaKraken" width="128" height="128">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">captchakraken</h1>
6
6
 
7
7
  <p align="center">
8
8
  <b>A captcha solver for browser automation.</b><br>
9
- The Python engine and CLI behind <a href="https://github.com/JWriter20/CaptchaKraken">CaptchaKraken</a>.
9
+ The Python engine and CLI behind <a href="https://github.com/CaptchaKraken/CaptchaKraken">CaptchaKraken</a>.
10
10
  </p>
11
11
 
12
12
  OpenCV tile detection plus a fine-tuned **Qwen3.5-9B** vision model. Give it a
@@ -19,34 +19,31 @@ nothing at all.
19
19
 
20
20
  > For demo videos, accuracy numbers, the browser driver, and the full
21
21
  > self-hosting guide, see the main repo
22
- > **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
22
+ > **[CaptchaKraken](https://github.com/CaptchaKraken/CaptchaKraken)**.
23
23
 
24
24
  ## Watch it work
25
25
 
26
26
  <p align="center">
27
- <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/hcaptcha_grid.webp" width="260"
28
- alt="A live hCaptcha image select challenge being solved end to end">
29
- <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/recaptcha_4x4.webp" width="260"
27
+ <img src="https://raw.githubusercontent.com/CaptchaKraken/CaptchaKraken/main/docs/assets/demo/recaptcha_4x4.webp" width="260"
30
28
  alt="A live reCAPTCHA 4×4 tile grid challenge being solved end to end">
31
- <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/geetest_slide.webp" width="260"
29
+ <img src="https://raw.githubusercontent.com/CaptchaKraken/CaptchaKraken/main/docs/assets/demo/geetest_slide.webp" width="260"
32
30
  alt="A live GeeTest slide jigsaw challenge being solved end to end">
33
31
  </p>
34
32
 
35
- hCaptcha image select 12/12 in 10.5s · reCAPTCHA 4×4 tile grid 9/10 in 8.7s · GeeTest slide jigsaw 10/10 in 7.6s — median of the solved attempts, measured 2026-08-19 on **captcha-v12**
33
+ reCAPTCHA 4×4 tile grid 9/10 in 8.7s · GeeTest slide jigsaw 10/10 in 7.6s — median of the solved attempts, measured 2026-08-19 on **captcha-v12**
36
34
  against each vendor's own public demo page. Counts rather than percentages
37
35
  because ten attempts is not a percentage. Idle time is cut from the clips, so
38
36
  they run shorter than the solves they show.
39
37
 
40
- **Ten more puzzle types**, as video and with the full method, at
38
+ **More puzzle types**, as video and with the full method, at
41
39
  [captchakraken.com](https://captchakraken.com) and in the
42
- [main repo](https://github.com/JWriter20/CaptchaKraken#watch-it-work).
40
+ [main repo](https://github.com/CaptchaKraken/CaptchaKraken#watch-it-work).
43
41
 
44
42
  ## What it solves
45
43
 
46
44
  | Vendor | Puzzles |
47
45
  |---|---|
48
46
  | **reCAPTCHA** | 3×3 and 4×4 image grids, including the dynamic re-deal |
49
- | **hCaptcha** | Image grids, click, drag, connect-the-path, tetris-fit, animated |
50
47
  | **GeeTest** v3 + v4 | Slide, icon, nine, svg, gobang, iconcrush |
51
48
  | **NetEase Yidun** | Jigsaw, picture-click, icon-click |
52
49
  | **Tencent, Lemin, Prosopo** | Slide, cropped-image and grid flows |
@@ -67,7 +64,7 @@ pip install "captchakraken[serve]" # + the serving stack (vLLM/torch) to self-
67
64
  The base install is lightweight — everything you need to solve captchas against
68
65
  a vLLM server (local or remote). The `[serve]` extra pulls the heavy stack only
69
66
  if you want to run the model yourself. The one-command
70
- [`setup.sh`](https://github.com/JWriter20/CaptchaKraken) installs `[serve]`,
67
+ [`setup.sh`](https://github.com/CaptchaKraken/CaptchaKraken) installs `[serve]`,
71
68
  downloads the weights, and writes an env file for you.
72
69
 
73
70
  ## No GPU? Use the hosted API
@@ -100,7 +97,7 @@ captchakraken server start | stop | status | run
100
97
  ```bash
101
98
  # Solve an image/video: classify → find_grid → plan. Prints the click actions.
102
99
  captchakraken path/to/captcha.png
103
- captchakraken path/to/captcha.png --puzzle-source hcaptcha
100
+ captchakraken path/to/captcha.png --puzzle-source recaptcha
104
101
  ```
105
102
 
106
103
  ```python
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "captchakraken"
7
- version = "3.0.0"
7
+ version = "3.2.0"
8
8
  description = "Captcha-solving engine and CLI: OpenCV grid detection plus a fine-tuned Qwen3.5-9B vision model, for image grids, click and drag puzzles, sliders, distorted text and animated challenges. Hosted API or self-hosted on vLLM."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -15,7 +15,7 @@ requires-python = ">=3.11"
15
15
  license = { file = "LICENSE" }
16
16
  authors = [{ name = "Jake Writer" }]
17
17
  keywords = [
18
- "captcha", "captcha-solver", "recaptcha", "hcaptcha", "geetest", "turnstile",
18
+ "captcha", "captcha-solver", "recaptcha", "geetest", "turnstile",
19
19
  "yidun", "tencent-captcha", "playwright", "browser-automation", "scraping",
20
20
  "anti-bot", "vllm", "qwen", "computer-vision", "ocr",
21
21
  ]
@@ -92,10 +92,10 @@ captchakraken = "captchakraken.cli:main"
92
92
 
93
93
  [project.urls]
94
94
  Homepage = "https://captchakraken.com"
95
- Documentation = "https://github.com/JWriter20/CaptchaKraken#readme"
96
- Source = "https://github.com/JWriter20/CaptchaKraken"
97
- Issues = "https://github.com/JWriter20/CaptchaKraken/issues"
98
- Changelog = "https://github.com/JWriter20/CaptchaKraken/blob/main/CHANGELOG.md"
95
+ Documentation = "https://github.com/CaptchaKraken/CaptchaKraken#readme"
96
+ Source = "https://github.com/CaptchaKraken/CaptchaKraken"
97
+ Issues = "https://github.com/CaptchaKraken/CaptchaKraken/issues"
98
+ Changelog = "https://github.com/CaptchaKraken/CaptchaKraken/blob/main/CHANGELOG.md"
99
99
 
100
100
  [tool.hatch.build.targets.wheel]
101
101
  packages = ["src/captchakraken"]
@@ -30,7 +30,9 @@ from .kinds import (
30
30
  PromptFamily,
31
31
  RetryMode,
32
32
  Vendor,
33
+ Verdict,
33
34
  )
35
+ from .verdicts import RoundVerdict
34
36
 
35
37
  from .humanize import (
36
38
  AppiumTouchBackend,
@@ -96,7 +98,9 @@ __all__ = [
96
98
  "PauseKind",
97
99
  "PromptFamily",
98
100
  "RetryMode",
101
+ "RoundVerdict",
99
102
  "Vendor",
103
+ "Verdict",
100
104
  ]
101
105
 
102
- __version__ = "3.0.0"
106
+ __version__ = "3.2.0"
@@ -177,7 +177,10 @@ def _report_outcome(args):
177
177
  try:
178
178
  from .planner import ActionPlanner
179
179
 
180
- return {"reported": bool(ActionPlanner().report_outcome(args[0], Outcome(args[1]) == Outcome.SOLVED))}
180
+ planner = ActionPlanner()
181
+ reported = planner.report_outcome(args[0], Outcome(args[1]) == Outcome.SOLVED)
182
+ # `supported` false is the self-hosted 404, which tells the driver to stop spawning this.
183
+ return {"reported": bool(reported), "supported": planner.outcome_supported}
181
184
  except Exception as exc:
182
185
  return {"reported": False, "error": str(exc)}
183
186
 
@@ -185,6 +188,7 @@ def _report_outcome(args):
185
188
  def _serve(args):
186
189
  """Persistent CV worker: one JSON request per stdin line, one JSON reply per stdout line."""
187
190
  from .image_processor import ImageProcessor
191
+ from .tool_calls.find_checkbox import find_checkbox
188
192
  from .tool_calls.find_grid import find_grid
189
193
 
190
194
  def handle(req):
@@ -206,6 +210,8 @@ def _serve(args):
206
210
  return match_region(req["ref"], req["live"], float(req["cx"]), float(req["cy"]), req.get("tolerance"))
207
211
  if cmd == "board-painted":
208
212
  return board_painted(req["image"], req.get("floor"), req.get("box"))
213
+ if cmd == "find-checkbox":
214
+ return find_checkbox(req["image"])
209
215
  if cmd == "track-piece":
210
216
  return track_piece(req["before"], req["after"], req.get("exclude"), req.get("travel") or 0.0)
211
217
  raise ValueError(f"unknown cmd: {cmd!r}")
@@ -104,6 +104,15 @@ class Outcome(StrEnum):
104
104
  FAILED = "failed"
105
105
 
106
106
 
107
+ class Verdict(StrEnum):
108
+ """What the vendor's own server answered a round: BLOCKED is a refusal to serve at all, not a wrong answer."""
109
+
110
+ ACCEPTED = "accepted"
111
+ REJECTED = "rejected"
112
+ NEW_CHALLENGE = "new-challenge"
113
+ BLOCKED = "blocked"
114
+
115
+
107
116
  class Phase(StrEnum):
108
117
  """Timing phases. Only INFERENCE and MOUSE are productive; everything else is waiting."""
109
118