cracked-ai 0.1.0__tar.gz

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.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: cracked-ai
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency Python client for Cracked, the agent tool router: discover, inspect, run and poll 50,000+ tools with one API key.
5
+ Author: Cracked
6
+ License: MIT
7
+ Project-URL: Homepage, https://cracked.ai
8
+ Project-URL: Documentation, https://cracked.ai/docs/sdks
9
+ Project-URL: API reference, https://cracked.ai/docs/api
10
+ Keywords: agents,tools,api,llm,scraping,mcp,cracked
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=7; extra == "test"
21
+
22
+ # cracked-ai (Python)
23
+
24
+ Zero-dependency Python client for [Cracked](https://cracked.ai), the tool router for agents: one API key, one balance, 50,000+ tools (scrapers, search, data, AI models). Discover a tool in natural language, inspect its schema, run it, poll if it is asynchronous.
25
+
26
+ Publishing to PyPI soon. Until then install from the repo path:
27
+
28
+ ```bash
29
+ pip install ./sdk/python # from the repo root
30
+ # or, once published:
31
+ pip install cracked-ai
32
+ ```
33
+
34
+ Python 3.9+. Uses only the standard library (`urllib`).
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import os
40
+ from cracked_ai import Cracked, CrackedError
41
+
42
+ c = Cracked() # reads CRACKED_API_KEY; base URL from CRACKED_BASE_URL (default https://cracked.ai/v1)
43
+
44
+ hits = c.discover("weather forecast for a lat/long", limit=5)["results"]
45
+ best = hits[0]
46
+ schema = c.inspect(best["provider"], best["endpoint"])["input"]["body"] # JSON Schema, never guess fields
47
+
48
+ rec = c.run("open-meteo", "/forecast", {"latitude": 30.27, "longitude": -97.74})
49
+ print(rec["status"], rec["billing"]["totalUsd"])
50
+ print(rec["output"])
51
+ ```
52
+
53
+ No key yet? Register an agent workspace with no human in the loop (trial credit included):
54
+
55
+ ```python
56
+ reg = Cracked.register_agent(name="my-agent")
57
+ reg.client.run(...) # ready to use
58
+ print(reg.api_key) # shown once: store it as CRACKED_API_KEY
59
+ print(reg.claim_url) # give this to your human: adds credit, unlocks top-ups
60
+ ```
61
+
62
+ ## Methods
63
+
64
+ | Method | Endpoint | Notes |
65
+ |---|---|---|
66
+ | `discover(query, limit=8, live_only=None, min_score=None, category=None, provider=None, include_apify=None, include_unavailable=None)` | `POST /discover` | Returns `{query, count, results}` |
67
+ | `inspect(provider, endpoint)` | `POST /inspect` | Schema in `["input"]["body"]`, price, health |
68
+ | `run(provider, endpoint, input=None, wait=True, poll=None, webhook_url=None, timeout=300, poll_wait=30)` | `POST /run` | Returns a `RunRecord`; polls on 202 when `poll` (defaults to `wait`) |
69
+ | `run_capability(capability, input=None, ...)` | `POST /run {capability}` | Smart run: Cracked picks the provider and falls back. Adds `routedTo`, `attempts` |
70
+ | `capabilities(id=None)` | `GET /capabilities?id=` | List capabilities (input schema + ranked candidates), or one by id. No auth |
71
+ | `get_run(run_id, wait=None)` | `GET /runs/{id}?wait=` | `wait` long-polls up to 100 s |
72
+ | `wait_for(run_id, timeout=300, poll_wait=30)` | polls `GET /runs/{id}` | Raises `CrackedTimeout` (with `.record`) if still RUNNING |
73
+ | `stop(run_id)` | `POST /runs/{id}/stop` | 409 if already finished |
74
+ | `runs(limit=25)` | `GET /runs` | Recent runs, no output bodies |
75
+ | `feedback(run_id, ok, note=None)` | `POST /runs/{id}/feedback` | Thumbs up/down; small bounty on claimed workspaces |
76
+ | `balance()` | `GET /wallet/balance` | `balance`, `lifetimeTopUp`, `lifetimeSpend` |
77
+ | `whoami()` | `GET /auth/whoami` | |
78
+ | `referrals()` / `referral_link()` | `GET /referrals` | Share the link: both sides get credit |
79
+ | `bounty_program()` | `GET /bounties/program` | Public, no auth |
80
+ | `bounties()` | `GET /bounties` | Your bounty ledger |
81
+ | `claim_bounty(url)` | `POST /bounties` | Backlink/listing bounty for a page carrying your referral link |
82
+ | `Cracked.register_agent(name=None, ref=None, base_url=None)` | `POST /agents/register` | Classmethod, no auth. Returns `AgentRegistration(client, api_key, claim_url, referral_url, ...)` |
83
+
84
+ All responses are plain `dict`s typed as `TypedDict`s (`RunRecord`, `SmartRunRecord`, `DiscoverResult`, `InspectResult`, `Balance`, ...). Import them from `cracked_ai`.
85
+
86
+ ## Async runs
87
+
88
+ Scrapers marked `async: true` in discover results can take minutes. Either let the client poll:
89
+
90
+ ```python
91
+ rec = c.run("apify", "/apify/instagram-profile-scraper", {"usernames": ["nike"]}, wait=False, poll=True, timeout=600)
92
+ ```
93
+
94
+ or drive it yourself:
95
+
96
+ ```python
97
+ rec = c.run("apify", "/apify/instagram-profile-scraper", {"usernames": ["nike"]}, wait=False) # HTTP 202, status RUNNING
98
+ rec = c.wait_for(rec["runId"], timeout=600) # or c.get_run(id, wait=30) in a loop
99
+ c.stop(rec["runId"]) # abort if needed
100
+ ```
101
+
102
+ Terminal statuses: `COMPLETED`, `FAILED`, `BLOCKED`, `STOPPED`, `TIMED_OUT`. `COMPLETED` with `providerResponse.httpStatus == 404` means the provider found nothing; that is not an error. `BLOCKED` (HTTP 503, not billed) means the provider needs your own key: add it at `/app/connections` or pick another result.
103
+
104
+ ## Errors
105
+
106
+ Every non-2xx response raises `CrackedError` with `.status`, `.code`, `.message` (and `.run_id` when relevant):
107
+
108
+ ```python
109
+ try:
110
+ c.run("some-provider", "/endpoint", {...})
111
+ except CrackedError as e:
112
+ if e.insufficient_funds: # 402: top up at https://cracked.ai/app/billing or earn credit via bounties
113
+ ...
114
+ elif e.unauthorized: # 401: bad or missing key
115
+ ...
116
+ elif e.rate_limited: # 429
117
+ ...
118
+ ```
119
+
120
+ Transport failures raise `CrackedError` with `status == 0` and code `NETWORK_ERROR` / `NETWORK_TIMEOUT`.
121
+
122
+ ## CLI
123
+
124
+ ```bash
125
+ python -m cracked_ai discover "instagram profile followers" -l 5
126
+ python -m cracked_ai inspect open-meteo /forecast
127
+ python -m cracked_ai run open-meteo /forecast '{"latitude":30.27,"longitude":-97.74}'
128
+ python -m cracked_ai capability instagram-profile '{"username":"nike"}'
129
+ python -m cracked_ai get <runId> -w 30
130
+ python -m cracked_ai balance
131
+ python -m cracked_ai register my-agent
132
+ ```
133
+
134
+ (`cracked-ai ...` works too once installed via pip.)
135
+
136
+ ## Tests
137
+
138
+ `tests/test_smoke.py` hits the live API and is skipped unless `CRACKED_API_KEY` is set. It spends well under $0.05 (two keyless weather calls).
139
+
140
+ ```bash
141
+ CRACKED_API_KEY=ck_live_... CRACKED_BASE_URL=https://cracked.ai/v1 python -m pytest sdk/python/tests -q
142
+ ```
143
+
144
+ ## Publishing
145
+
146
+ ```bash
147
+ cd sdk/python && python -m pip install build twine && python -m build && twine upload dist/*
148
+ ```
@@ -0,0 +1,127 @@
1
+ # cracked-ai (Python)
2
+
3
+ Zero-dependency Python client for [Cracked](https://cracked.ai), the tool router for agents: one API key, one balance, 50,000+ tools (scrapers, search, data, AI models). Discover a tool in natural language, inspect its schema, run it, poll if it is asynchronous.
4
+
5
+ Publishing to PyPI soon. Until then install from the repo path:
6
+
7
+ ```bash
8
+ pip install ./sdk/python # from the repo root
9
+ # or, once published:
10
+ pip install cracked-ai
11
+ ```
12
+
13
+ Python 3.9+. Uses only the standard library (`urllib`).
14
+
15
+ ## Quick start
16
+
17
+ ```python
18
+ import os
19
+ from cracked_ai import Cracked, CrackedError
20
+
21
+ c = Cracked() # reads CRACKED_API_KEY; base URL from CRACKED_BASE_URL (default https://cracked.ai/v1)
22
+
23
+ hits = c.discover("weather forecast for a lat/long", limit=5)["results"]
24
+ best = hits[0]
25
+ schema = c.inspect(best["provider"], best["endpoint"])["input"]["body"] # JSON Schema, never guess fields
26
+
27
+ rec = c.run("open-meteo", "/forecast", {"latitude": 30.27, "longitude": -97.74})
28
+ print(rec["status"], rec["billing"]["totalUsd"])
29
+ print(rec["output"])
30
+ ```
31
+
32
+ No key yet? Register an agent workspace with no human in the loop (trial credit included):
33
+
34
+ ```python
35
+ reg = Cracked.register_agent(name="my-agent")
36
+ reg.client.run(...) # ready to use
37
+ print(reg.api_key) # shown once: store it as CRACKED_API_KEY
38
+ print(reg.claim_url) # give this to your human: adds credit, unlocks top-ups
39
+ ```
40
+
41
+ ## Methods
42
+
43
+ | Method | Endpoint | Notes |
44
+ |---|---|---|
45
+ | `discover(query, limit=8, live_only=None, min_score=None, category=None, provider=None, include_apify=None, include_unavailable=None)` | `POST /discover` | Returns `{query, count, results}` |
46
+ | `inspect(provider, endpoint)` | `POST /inspect` | Schema in `["input"]["body"]`, price, health |
47
+ | `run(provider, endpoint, input=None, wait=True, poll=None, webhook_url=None, timeout=300, poll_wait=30)` | `POST /run` | Returns a `RunRecord`; polls on 202 when `poll` (defaults to `wait`) |
48
+ | `run_capability(capability, input=None, ...)` | `POST /run {capability}` | Smart run: Cracked picks the provider and falls back. Adds `routedTo`, `attempts` |
49
+ | `capabilities(id=None)` | `GET /capabilities?id=` | List capabilities (input schema + ranked candidates), or one by id. No auth |
50
+ | `get_run(run_id, wait=None)` | `GET /runs/{id}?wait=` | `wait` long-polls up to 100 s |
51
+ | `wait_for(run_id, timeout=300, poll_wait=30)` | polls `GET /runs/{id}` | Raises `CrackedTimeout` (with `.record`) if still RUNNING |
52
+ | `stop(run_id)` | `POST /runs/{id}/stop` | 409 if already finished |
53
+ | `runs(limit=25)` | `GET /runs` | Recent runs, no output bodies |
54
+ | `feedback(run_id, ok, note=None)` | `POST /runs/{id}/feedback` | Thumbs up/down; small bounty on claimed workspaces |
55
+ | `balance()` | `GET /wallet/balance` | `balance`, `lifetimeTopUp`, `lifetimeSpend` |
56
+ | `whoami()` | `GET /auth/whoami` | |
57
+ | `referrals()` / `referral_link()` | `GET /referrals` | Share the link: both sides get credit |
58
+ | `bounty_program()` | `GET /bounties/program` | Public, no auth |
59
+ | `bounties()` | `GET /bounties` | Your bounty ledger |
60
+ | `claim_bounty(url)` | `POST /bounties` | Backlink/listing bounty for a page carrying your referral link |
61
+ | `Cracked.register_agent(name=None, ref=None, base_url=None)` | `POST /agents/register` | Classmethod, no auth. Returns `AgentRegistration(client, api_key, claim_url, referral_url, ...)` |
62
+
63
+ All responses are plain `dict`s typed as `TypedDict`s (`RunRecord`, `SmartRunRecord`, `DiscoverResult`, `InspectResult`, `Balance`, ...). Import them from `cracked_ai`.
64
+
65
+ ## Async runs
66
+
67
+ Scrapers marked `async: true` in discover results can take minutes. Either let the client poll:
68
+
69
+ ```python
70
+ rec = c.run("apify", "/apify/instagram-profile-scraper", {"usernames": ["nike"]}, wait=False, poll=True, timeout=600)
71
+ ```
72
+
73
+ or drive it yourself:
74
+
75
+ ```python
76
+ rec = c.run("apify", "/apify/instagram-profile-scraper", {"usernames": ["nike"]}, wait=False) # HTTP 202, status RUNNING
77
+ rec = c.wait_for(rec["runId"], timeout=600) # or c.get_run(id, wait=30) in a loop
78
+ c.stop(rec["runId"]) # abort if needed
79
+ ```
80
+
81
+ Terminal statuses: `COMPLETED`, `FAILED`, `BLOCKED`, `STOPPED`, `TIMED_OUT`. `COMPLETED` with `providerResponse.httpStatus == 404` means the provider found nothing; that is not an error. `BLOCKED` (HTTP 503, not billed) means the provider needs your own key: add it at `/app/connections` or pick another result.
82
+
83
+ ## Errors
84
+
85
+ Every non-2xx response raises `CrackedError` with `.status`, `.code`, `.message` (and `.run_id` when relevant):
86
+
87
+ ```python
88
+ try:
89
+ c.run("some-provider", "/endpoint", {...})
90
+ except CrackedError as e:
91
+ if e.insufficient_funds: # 402: top up at https://cracked.ai/app/billing or earn credit via bounties
92
+ ...
93
+ elif e.unauthorized: # 401: bad or missing key
94
+ ...
95
+ elif e.rate_limited: # 429
96
+ ...
97
+ ```
98
+
99
+ Transport failures raise `CrackedError` with `status == 0` and code `NETWORK_ERROR` / `NETWORK_TIMEOUT`.
100
+
101
+ ## CLI
102
+
103
+ ```bash
104
+ python -m cracked_ai discover "instagram profile followers" -l 5
105
+ python -m cracked_ai inspect open-meteo /forecast
106
+ python -m cracked_ai run open-meteo /forecast '{"latitude":30.27,"longitude":-97.74}'
107
+ python -m cracked_ai capability instagram-profile '{"username":"nike"}'
108
+ python -m cracked_ai get <runId> -w 30
109
+ python -m cracked_ai balance
110
+ python -m cracked_ai register my-agent
111
+ ```
112
+
113
+ (`cracked-ai ...` works too once installed via pip.)
114
+
115
+ ## Tests
116
+
117
+ `tests/test_smoke.py` hits the live API and is skipped unless `CRACKED_API_KEY` is set. It spends well under $0.05 (two keyless weather calls).
118
+
119
+ ```bash
120
+ CRACKED_API_KEY=ck_live_... CRACKED_BASE_URL=https://cracked.ai/v1 python -m pytest sdk/python/tests -q
121
+ ```
122
+
123
+ ## Publishing
124
+
125
+ ```bash
126
+ cd sdk/python && python -m pip install build twine && python -m build && twine upload dist/*
127
+ ```
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cracked-ai"
7
+ version = "0.1.0"
8
+ description = "Zero-dependency Python client for Cracked, the agent tool router: discover, inspect, run and poll 50,000+ tools with one API key."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Cracked" }]
13
+ keywords = ["agents", "tools", "api", "llm", "scraping", "mcp", "cracked"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://cracked.ai"
26
+ Documentation = "https://cracked.ai/docs/sdks"
27
+ "API reference" = "https://cracked.ai/docs/api"
28
+
29
+ [project.scripts]
30
+ cracked-ai = "cracked_ai.__main__:main"
31
+
32
+ [project.optional-dependencies]
33
+ test = ["pytest>=7"]
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ cracked_ai = ["py.typed"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,69 @@
1
+ """cracked_ai: zero-dependency Python client for Cracked, the agent tool router.
2
+
3
+ from cracked_ai import Cracked
4
+ c = Cracked() # CRACKED_API_KEY, CRACKED_BASE_URL
5
+ rec = c.run("open-meteo", "/forecast", {"latitude": 30.27, "longitude": -97.74})
6
+ """
7
+
8
+ from .client import (
9
+ DEFAULT_BASE_URL,
10
+ SERVER_MAX_POLL_WAIT,
11
+ TERMINAL_STATUSES,
12
+ AgentRegistration,
13
+ Cracked,
14
+ __version__,
15
+ is_terminal,
16
+ )
17
+ from .errors import CrackedError, CrackedTimeout
18
+ from .types import (
19
+ Balance,
20
+ Billing,
21
+ BountyClaim,
22
+ DiscoverResponse,
23
+ DiscoverResult,
24
+ FeedbackResult,
25
+ HealthSummary,
26
+ InspectResult,
27
+ Price,
28
+ ProviderResponse,
29
+ ReferralStats,
30
+ RegistrationResponse,
31
+ RouteAttempt,
32
+ RoutedTo,
33
+ RunList,
34
+ RunRecord,
35
+ RunStatus,
36
+ SmartRunRecord,
37
+ StopResult,
38
+ )
39
+
40
+ __all__ = [
41
+ "Cracked",
42
+ "AgentRegistration",
43
+ "CrackedError",
44
+ "CrackedTimeout",
45
+ "DEFAULT_BASE_URL",
46
+ "SERVER_MAX_POLL_WAIT",
47
+ "TERMINAL_STATUSES",
48
+ "is_terminal",
49
+ "__version__",
50
+ "Balance",
51
+ "Billing",
52
+ "BountyClaim",
53
+ "DiscoverResponse",
54
+ "DiscoverResult",
55
+ "FeedbackResult",
56
+ "HealthSummary",
57
+ "InspectResult",
58
+ "Price",
59
+ "ProviderResponse",
60
+ "ReferralStats",
61
+ "RegistrationResponse",
62
+ "RouteAttempt",
63
+ "RoutedTo",
64
+ "RunList",
65
+ "RunRecord",
66
+ "RunStatus",
67
+ "SmartRunRecord",
68
+ "StopResult",
69
+ ]
@@ -0,0 +1,105 @@
1
+ """Minimal CLI: ``python -m cracked_ai discover "instagram followers"``.
2
+
3
+ Commands: discover, inspect, run, capability, get, stop, balance, referral, register.
4
+ Set CRACKED_API_KEY (and optionally CRACKED_BASE_URL). Output is JSON.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+
13
+ from .client import Cracked
14
+ from .errors import CrackedError
15
+
16
+
17
+ def _json_arg(s: str | None) -> dict:
18
+ if not s:
19
+ return {}
20
+ if s.startswith("@"):
21
+ with open(s[1:], "r", encoding="utf-8") as f:
22
+ return json.load(f)
23
+ return json.loads(s)
24
+
25
+
26
+ def main(argv: list[str] | None = None) -> int:
27
+ p = argparse.ArgumentParser(prog="python -m cracked_ai", description="Cracked tool router CLI (JSON output)")
28
+ p.add_argument("--base-url", help="override CRACKED_BASE_URL")
29
+ p.add_argument("--api-key", help="override CRACKED_API_KEY")
30
+ sub = p.add_subparsers(dest="cmd", required=True)
31
+
32
+ d = sub.add_parser("discover", help="search tools in natural language")
33
+ d.add_argument("query", nargs="+")
34
+ d.add_argument("-l", "--limit", type=int, default=8)
35
+ d.add_argument("--live", action="store_true", help="only providers that run without your own key")
36
+
37
+ i = sub.add_parser("inspect", help="schema, price, health")
38
+ i.add_argument("provider")
39
+ i.add_argument("endpoint")
40
+
41
+ r = sub.add_parser("run", help="execute an endpoint")
42
+ r.add_argument("provider")
43
+ r.add_argument("endpoint")
44
+ r.add_argument("input", nargs="?", help='JSON object or @file.json')
45
+ r.add_argument("--no-wait", action="store_true", help="return the 202 immediately, do not poll")
46
+ r.add_argument("--timeout", type=float, default=300)
47
+
48
+ cap = sub.add_parser("capability", help="smart run by capability name, e.g. instagram-profile")
49
+ cap.add_argument("capability")
50
+ cap.add_argument("input", nargs="?", help='JSON object or @file.json')
51
+ cap.add_argument("--no-wait", action="store_true")
52
+ cap.add_argument("--timeout", type=float, default=300)
53
+
54
+ g = sub.add_parser("get", help="fetch a run")
55
+ g.add_argument("run_id")
56
+ g.add_argument("-w", "--wait", type=int, default=0, help="long-poll seconds (max 100)")
57
+
58
+ s = sub.add_parser("stop", help="abort a running job")
59
+ s.add_argument("run_id")
60
+
61
+ sub.add_parser("balance", help="wallet balance")
62
+ sub.add_parser("referral", help="referral link and earnings")
63
+ sub.add_parser("program", help="bounty program (no auth)")
64
+
65
+ reg = sub.add_parser("register", help="self-register an agent workspace and print the key once")
66
+ reg.add_argument("name", nargs="?")
67
+ reg.add_argument("--ref")
68
+
69
+ a = p.parse_args(argv)
70
+ try:
71
+ if a.cmd == "register":
72
+ out = Cracked.register_agent(a.name, a.ref, base_url=a.base_url).raw
73
+ else:
74
+ c = Cracked(api_key=a.api_key, base_url=a.base_url)
75
+ if a.cmd == "discover":
76
+ out = c.discover(" ".join(a.query), limit=a.limit, live_only=True if a.live else None)
77
+ elif a.cmd == "inspect":
78
+ out = c.inspect(a.provider, a.endpoint)
79
+ elif a.cmd == "run":
80
+ out = c.run(a.provider, a.endpoint, _json_arg(a.input), wait=not a.no_wait, timeout=a.timeout)
81
+ elif a.cmd == "capability":
82
+ out = c.run_capability(a.capability, _json_arg(a.input), wait=not a.no_wait, timeout=a.timeout)
83
+ elif a.cmd == "get":
84
+ out = c.get_run(a.run_id, wait=a.wait or None)
85
+ elif a.cmd == "stop":
86
+ out = c.stop(a.run_id)
87
+ elif a.cmd == "balance":
88
+ out = c.balance()
89
+ elif a.cmd == "referral":
90
+ out = c.referrals()
91
+ elif a.cmd == "program":
92
+ out = c.bounty_program()
93
+ else: # pragma: no cover
94
+ p.error("unknown command")
95
+ json.dump(out, sys.stdout, indent=2)
96
+ sys.stdout.write("\n")
97
+ return 0
98
+ except CrackedError as e:
99
+ json.dump({"error": e.code, "status": e.status, "message": e.message, **({"runId": e.run_id} if e.run_id else {})}, sys.stderr, indent=2)
100
+ sys.stderr.write("\n")
101
+ return 2
102
+
103
+
104
+ if __name__ == "__main__":
105
+ sys.exit(main())