captchakraken 2.3.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 (54) 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/errors.d.ts +45 -0
  6. package/dist/errors.js +74 -0
  7. package/dist/index.d.ts +50 -0
  8. package/dist/index.js +52 -1
  9. package/dist/limits.d.ts +51 -0
  10. package/dist/limits.js +54 -0
  11. package/dist/model-name.d.ts +23 -0
  12. package/dist/model-name.js +70 -0
  13. package/dist/playwright-types.d.ts +28 -0
  14. package/dist/puppeteer-adapter.d.ts +11 -0
  15. package/dist/puppeteer-adapter.js +23 -0
  16. package/dist/python-command.d.ts +50 -0
  17. package/dist/python-command.js +60 -0
  18. package/dist/slide-geometry.d.ts +33 -0
  19. package/dist/slide-geometry.js +64 -0
  20. package/dist/solver.d.ts +214 -12
  21. package/dist/solver.js +1297 -165
  22. package/dist/trajectory.d.ts +31 -0
  23. package/dist/trajectory.js +162 -0
  24. package/dist/types.d.ts +128 -11
  25. package/dist/watcher.d.ts +99 -0
  26. package/dist/watcher.js +129 -0
  27. package/package.json +2 -5
  28. package/python/LICENSE +47 -7
  29. package/python/README.md +25 -8
  30. package/python/examples/README.md +36 -0
  31. package/python/examples/_harness.py +182 -94
  32. package/python/examples/demoHcaptcha.py +2 -1
  33. package/python/examples/demoRecaptcha.py +2 -1
  34. package/python/examples/watch_playwright.py +56 -0
  35. package/python/examples/with_playwright.py +43 -0
  36. package/python/pyproject.toml +4 -3
  37. package/python/src/captchakraken/__init__.py +5 -3
  38. package/python/src/captchakraken/action_types.py +24 -3
  39. package/python/src/captchakraken/cli.py +224 -2
  40. package/python/src/captchakraken/config.py +119 -17
  41. package/python/src/captchakraken/errors.py +195 -0
  42. package/python/src/captchakraken/keyframes.py +690 -0
  43. package/python/src/captchakraken/models.json +120 -0
  44. package/python/src/captchakraken/page_solver.py +1465 -102
  45. package/python/src/captchakraken/pinned_model.json +5 -7
  46. package/python/src/captchakraken/planner.py +328 -73
  47. package/python/src/captchakraken/prompts.py +485 -0
  48. package/python/src/captchakraken/solver.py +182 -11
  49. package/python/src/captchakraken/timing.py +70 -0
  50. package/python/src/captchakraken/tool_calls/find_grid.py +367 -52
  51. package/python/src/captchakraken/tool_calls/track_piece.py +96 -0
  52. package/python/src/captchakraken/trajectory.py +2 -7
  53. package/python/src/captchakraken/watcher.py +193 -0
  54. package/python/src/captchakraken/planner_types.py +0 -74
@@ -0,0 +1,195 @@
1
+ """What the hosted API's refusals mean, in words a user can act on.
2
+
3
+ The solver talks to an OpenAI-compatible endpoint that may be a local vLLM the
4
+ user started themselves or the hosted CaptchaKraken gateway. Those two fail very
5
+ differently, and until this module existed both were reported the same way:
6
+ `vLLM 402 Payment Required at https://api.captchakraken.com/v1/chat/completions`.
7
+ A camoufox user who has never heard of vLLM, run vLLM, or intended to run vLLM
8
+ would hit that on the day their credits ran out.
9
+
10
+ Two rules, both inherited from the gateway's own `src/errors.ts`:
11
+
12
+ 1. BRANCH ON `error.code`, NEVER ON PROSE. The messages get reworded; the codes
13
+ are the contract. A client that pattern-matches "Out of credits" breaks the
14
+ day someone improves the wording.
15
+
16
+ 2. AN UNRECOGNISED CODE MUST STILL PRODUCE A USEFUL MESSAGE. There are ten
17
+ codes today and there will be more. Enumerating them exhaustively here means
18
+ the eleventh is reported worse than the ten — so the fallback carries the
19
+ server's own `message` and `resolution_url` through, and only the phrasing
20
+ is lost.
21
+
22
+ The self-hosted path is deliberately untouched: a local vLLM does not send this
23
+ envelope, so `from_response` returns a message shaped like the old one and the
24
+ 401/403 hint about the bearer token survives. Nobody self-hosting sees a word
25
+ about credits.
26
+ """
27
+
28
+ from typing import Any, Dict, Optional
29
+
30
+ # Where a user with no `resolution_url` in hand should be sent. Only used as a
31
+ # fallback — the server almost always supplies a deep link that is better than
32
+ # this, and when it does we prefer it.
33
+ _DASHBOARD = "https://captchakraken.com/dashboard"
34
+ _SUPPORT = "https://captchakraken.com/support"
35
+
36
+
37
+ class CaptchaKrakenAPIError(RuntimeError):
38
+ """A refusal from the hosted API, already translated.
39
+
40
+ Carries the machine-readable parts alongside the message so a caller that
41
+ wants to react programmatically (retry on `rate_limited`, stop on
42
+ `insufficient_credits`) can do so without re-parsing anything. `str(e)` is
43
+ the human sentence and is what reaches a console.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ message: str,
49
+ *,
50
+ status: Optional[int] = None,
51
+ code: Optional[str] = None,
52
+ resolution_url: Optional[str] = None,
53
+ retry_after_seconds: Optional[float] = None,
54
+ ):
55
+ super().__init__(message)
56
+ self.status = status
57
+ self.code = code
58
+ self.resolution_url = resolution_url
59
+ self.retry_after_seconds = retry_after_seconds
60
+
61
+ def to_payload(self) -> Dict[str, Any]:
62
+ """The shape the CLI puts on stderr for the JS driver to pick up.
63
+
64
+ The JS half shells out to this CLI and can only see stdout/stderr, so
65
+ the structured fields would otherwise be flattened to a string at the
66
+ process boundary and have to be re-parsed out of prose — exactly the
67
+ thing rule 1 forbids.
68
+ """
69
+ return {
70
+ "error": str(self),
71
+ "ck_error": {
72
+ "status": self.status,
73
+ "code": self.code,
74
+ "resolution_url": self.resolution_url,
75
+ "retry_after_seconds": self.retry_after_seconds,
76
+ },
77
+ }
78
+
79
+
80
+ def _retry_after(headers: Any) -> Optional[float]:
81
+ """Seconds from the `Retry-After` header, if it is the numeric form.
82
+
83
+ HTTP also permits an HTTP-date here. The gateway only ever sends deltas, and
84
+ guessing wrong about a date would produce a confidently incorrect "wait 3
85
+ seconds", so anything unparseable is dropped rather than approximated.
86
+ """
87
+ try:
88
+ raw = headers.get("Retry-After")
89
+ except Exception: # noqa: BLE001 — a mapping-ish object is all we assume
90
+ return None
91
+ if raw is None:
92
+ return None
93
+ try:
94
+ return float(str(raw).strip())
95
+ except ValueError:
96
+ return None
97
+
98
+
99
+ def _sentence(code: str, message: str, url: Optional[str], retry: Optional[float]) -> str:
100
+ """The user-facing line for a known code.
101
+
102
+ Every branch names CaptchaKraken. That is the entire point of this module:
103
+ the person reading it needs to know which product is refusing them before
104
+ anything else in the sentence can help.
105
+ """
106
+ if code == "insufficient_credits":
107
+ return (
108
+ "CaptchaKraken: your account is out of credits, so this solve was refused. "
109
+ f"Top up at {url or _DASHBOARD} and retry."
110
+ )
111
+
112
+ if code == "solve_abandoned":
113
+ return (
114
+ "CaptchaKraken: this captcha attempt was served too many times without "
115
+ "settling and has been abandoned. That usually means the IP reputation or "
116
+ "the browser fingerprint is being rejected — not that the answers were "
117
+ "wrong. Start a fresh attempt with a new x-ck-session (the driver mints "
118
+ "one per solve() call, so a new solve is enough)."
119
+ )
120
+
121
+ if code == "rate_limited":
122
+ wait = f" Retry in about {retry:g}s." if retry else " Back off and retry."
123
+ return f"CaptchaKraken: too many requests.{wait}"
124
+
125
+ if code == "account_suspended":
126
+ return (
127
+ "CaptchaKraken: this account is suspended, so solving is disabled. "
128
+ f"Contact support at {url or _SUPPORT}."
129
+ )
130
+
131
+ if code == "request_too_large":
132
+ return (
133
+ "CaptchaKraken: the screenshot sent for this solve exceeded the request "
134
+ f"size limit. ({message}) Capture the captcha element rather than the "
135
+ "whole page if you are not already."
136
+ )
137
+
138
+ if code in ("missing_api_key", "invalid_api_key"):
139
+ return (
140
+ "CaptchaKraken: the API key was missing or not accepted. Set "
141
+ "CAPTCHA_KRAKEN_API_KEY, or run the CaptchaKraken MCP server's "
142
+ "`create_api_key` tool to write one to ~/.captchakraken/credentials. "
143
+ f"Manage keys at {url or _DASHBOARD}."
144
+ )
145
+
146
+ if code == "upstream_unavailable":
147
+ return (
148
+ "CaptchaKraken: the solver fleet is temporarily unreachable. This is on "
149
+ "our side, not yours — retry shortly."
150
+ )
151
+
152
+ # `unrecognized_prompt`, `invalid_request`, and anything added after this was
153
+ # written. The server's own message is the best thing available, so it is
154
+ # carried through verbatim rather than replaced with a guess.
155
+ tail = f" See {url}." if url else ""
156
+ return f"CaptchaKraken: {message}{tail}"
157
+
158
+
159
+ def from_response(resp: Any, url: str) -> Exception:
160
+ """Build the exception for a non-OK response, hosted or self-hosted.
161
+
162
+ Returns rather than raises so the caller's `raise` keeps the traceback
163
+ anchored at the request site, where it is useful.
164
+ """
165
+ body_text = (getattr(resp, "text", "") or "")[:300]
166
+ status = getattr(resp, "status_code", None)
167
+
168
+ error: Dict[str, Any] = {}
169
+ try:
170
+ parsed = resp.json()
171
+ if isinstance(parsed, dict) and isinstance(parsed.get("error"), dict):
172
+ error = parsed["error"]
173
+ except Exception: # noqa: BLE001 — a non-JSON body is the self-hosted case
174
+ error = {}
175
+
176
+ code = error.get("code")
177
+ if isinstance(code, str) and code:
178
+ retry = _retry_after(getattr(resp, "headers", {}) or {})
179
+ resolution = error.get("resolution_url") or None
180
+ message = str(error.get("message") or "").strip() or "the request was refused"
181
+ return CaptchaKrakenAPIError(
182
+ _sentence(code, message, resolution, retry),
183
+ status=status,
184
+ code=code,
185
+ resolution_url=resolution,
186
+ retry_after_seconds=retry,
187
+ )
188
+
189
+ # No gateway envelope: a local vLLM, a proxy in between, or an HTML error
190
+ # page. Keep the pre-existing message so self-hosted debugging is unchanged.
191
+ hint = ""
192
+ if status in (401, 403):
193
+ hint = " — check CAPTCHA_KRAKEN_API_KEY is set and forwarded to the CLI"
194
+ reason = getattr(resp, "reason", "") or ""
195
+ return RuntimeError(f"vLLM {status} {reason} at {url}{hint}. Body: {body_text}")