ceki-sdk 2.15.1__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.
Files changed (68) hide show
  1. ceki_sdk-2.15.1/.github/workflows/ci.yml +31 -0
  2. ceki_sdk-2.15.1/.github/workflows/publish.yml +28 -0
  3. ceki_sdk-2.15.1/.gitignore +17 -0
  4. ceki_sdk-2.15.1/LICENSE +21 -0
  5. ceki_sdk-2.15.1/PKG-INFO +294 -0
  6. ceki_sdk-2.15.1/README.md +265 -0
  7. ceki_sdk-2.15.1/SMOKE.md +102 -0
  8. ceki_sdk-2.15.1/ceki_sdk/__init__.py +52 -0
  9. ceki_sdk-2.15.1/ceki_sdk/_browser.py +646 -0
  10. ceki_sdk-2.15.1/ceki_sdk/_captcha.py +88 -0
  11. ceki_sdk-2.15.1/ceki_sdk/_chat.py +203 -0
  12. ceki_sdk-2.15.1/ceki_sdk/_client.py +501 -0
  13. ceki_sdk-2.15.1/ceki_sdk/_config.py +19 -0
  14. ceki_sdk-2.15.1/ceki_sdk/_connect.py +32 -0
  15. ceki_sdk-2.15.1/ceki_sdk/_exceptions.py +70 -0
  16. ceki_sdk-2.15.1/ceki_sdk/_models.py +88 -0
  17. ceki_sdk-2.15.1/ceki_sdk/_profile.py +137 -0
  18. ceki_sdk-2.15.1/ceki_sdk/_state.py +44 -0
  19. ceki_sdk-2.15.1/ceki_sdk/cli.py +583 -0
  20. ceki_sdk-2.15.1/ceki_sdk/humanize/__init__.py +4 -0
  21. ceki_sdk-2.15.1/ceki_sdk/humanize/humanizer.py +56 -0
  22. ceki_sdk-2.15.1/ceki_sdk/humanize/keymap.py +70 -0
  23. ceki_sdk-2.15.1/ceki_sdk/humanize/profile.py +96 -0
  24. ceki_sdk-2.15.1/ceki_sdk/humanize/profiles/careful.json +30 -0
  25. ceki_sdk-2.15.1/ceki_sdk/humanize/profiles/natural.json +30 -0
  26. ceki_sdk-2.15.1/examples/SMOKE.md +61 -0
  27. ceki_sdk-2.15.1/examples/__init__.py +0 -0
  28. ceki_sdk-2.15.1/examples/captcha_helper.py +41 -0
  29. ceki_sdk-2.15.1/examples/github_signup.py +203 -0
  30. ceki_sdk-2.15.1/examples/hello.py +22 -0
  31. ceki_sdk-2.15.1/examples/imap_helper.py +69 -0
  32. ceki_sdk-2.15.1/examples/navigate.py +41 -0
  33. ceki_sdk-2.15.1/examples/reddit_signup.py +205 -0
  34. ceki_sdk-2.15.1/examples/scraping.py +21 -0
  35. ceki_sdk-2.15.1/examples/smoke/README.md +74 -0
  36. ceki_sdk-2.15.1/examples/smoke/__init__.py +0 -0
  37. ceki_sdk-2.15.1/examples/smoke/mvp_smoke_v2.py +466 -0
  38. ceki_sdk-2.15.1/pyproject.toml +52 -0
  39. ceki_sdk-2.15.1/tests/__init__.py +0 -0
  40. ceki_sdk-2.15.1/tests/conftest.py +66 -0
  41. ceki_sdk-2.15.1/tests/e2e/README.md +43 -0
  42. ceki_sdk-2.15.1/tests/e2e/test_fingerprint_persistence.py +188 -0
  43. ceki_sdk-2.15.1/tests/test_browser_cdp.py +122 -0
  44. ceki_sdk-2.15.1/tests/test_browser_errors.py +138 -0
  45. ceki_sdk-2.15.1/tests/test_browser_release_alias.py +41 -0
  46. ceki_sdk-2.15.1/tests/test_browser_screenshot_format.py +108 -0
  47. ceki_sdk-2.15.1/tests/test_captcha.py +369 -0
  48. ceki_sdk-2.15.1/tests/test_chat.py +246 -0
  49. ceki_sdk-2.15.1/tests/test_chat_history.py +136 -0
  50. ceki_sdk-2.15.1/tests/test_chat_image.py +135 -0
  51. ceki_sdk-2.15.1/tests/test_cli.py +436 -0
  52. ceki_sdk-2.15.1/tests/test_connect.py +48 -0
  53. ceki_sdk-2.15.1/tests/test_error_mapping.py +164 -0
  54. ceki_sdk-2.15.1/tests/test_examples_signature.py +25 -0
  55. ceki_sdk-2.15.1/tests/test_humanize_browser.py +127 -0
  56. ceki_sdk-2.15.1/tests/test_multi_session.py +129 -0
  57. ceki_sdk-2.15.1/tests/test_profile.py +277 -0
  58. ceki_sdk-2.15.1/tests/test_provider_disconnect.py +117 -0
  59. ceki_sdk-2.15.1/tests/test_provider_offline.py +59 -0
  60. ceki_sdk-2.15.1/tests/test_reconnect.py +57 -0
  61. ceki_sdk-2.15.1/tests/test_rent_flow.py +183 -0
  62. ceki_sdk-2.15.1/tests/test_search.py +157 -0
  63. ceki_sdk-2.15.1/tests/test_sessions.py +106 -0
  64. ceki_sdk-2.15.1/tests/test_state_persistence.py +112 -0
  65. ceki_sdk-2.15.1/tests/test_switch_tab.py +65 -0
  66. ceki_sdk-2.15.1/tests/test_type_keyboard_events.py +95 -0
  67. ceki_sdk-2.15.1/tests/test_type_with_pointer.py +144 -0
  68. ceki_sdk-2.15.1/tests/test_upload.py +282 -0
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, "feature/**"]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Lint
28
+ run: ruff check .
29
+
30
+ - name: Test
31
+ run: pytest -v
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Build
21
+ run: |
22
+ pip install build
23
+ python -m build
24
+
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
+ with:
28
+ attestations: false
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .env
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ *.so
15
+ .coverage
16
+ htmlcov/
17
+ examples/*.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ceki.me
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,294 @@
1
+ Metadata-Version: 2.4
2
+ Name: ceki-sdk
3
+ Version: 2.15.1
4
+ Summary: Python SDK for browser.ceki.me — rent real browsers from real people
5
+ Project-URL: Homepage, https://ceki.me
6
+ Project-URL: Repository, https://github.com/Ceki-me/python-sdk
7
+ Author-email: "Ceki.me" <hello@ceki.me>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-agent,automation,browser,ceki,websocket
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pydantic>=2
22
+ Requires-Dist: websockets>=12
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.10; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: ruff>=0.5; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ceki-sdk
31
+
32
+ Python SDK for [browser.ceki.me](https://browser.ceki.me) — rent real browsers from real people for AI agent automation.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install ceki-sdk
38
+ ```
39
+
40
+ ## Quickstart
41
+
42
+ ```python
43
+ import asyncio
44
+ import os
45
+ from ceki_sdk import connect, ConnectOptions
46
+
47
+ async def main():
48
+ client = await connect(os.environ["CEKI_API_KEY"])
49
+ options = await client.search({"geo": "US", "language": "en"})
50
+ browser = await client.rent(options[0].schedule_id)
51
+ # ... CDP calls (see docs)
52
+ await browser.close()
53
+ await client.close()
54
+
55
+ asyncio.run(main())
56
+ ```
57
+
58
+ **BREAKING in 2.2.0:** `connect()` no longer accepts `relay_url=` or `reconnect=` kwargs — pass a `ConnectOptions` object instead.
59
+
60
+ ## Environment Variables
61
+
62
+ | Variable | Description |
63
+ |---|---|
64
+ | `CEKI_API_KEY` | Your API key (required) |
65
+
66
+ ## API
67
+
68
+ ### `connect(api_key, options: ConnectOptions | None = None) -> Client`
69
+
70
+ Establish a WebSocket connection to the relay. Returns a `Client` instance.
71
+
72
+ ### `ConnectOptions`
73
+
74
+ | Field | Default | Description |
75
+ |---|---|---|
76
+ | `reconnect` | `True` | Auto-reconnect on disconnect |
77
+
78
+ ### `client.search(filters=None, limit=20) -> list[BrowserOption]`
79
+
80
+ Search for available browsers. Filters: `geo`, `language`, etc.
81
+
82
+ ### `client.rent(schedule_id) -> Browser`
83
+
84
+ Rent a browser by schedule ID. Waits up to 60s for a match.
85
+
86
+ ### `client.close()`
87
+
88
+ Close all sessions and the connection.
89
+
90
+ ## Error Codes
91
+
92
+ | Exception | Cause |
93
+ |---|---|
94
+ | `AuthFailed` | Invalid API key or token revoked |
95
+ | `RateLimitExceeded` | Too many requests. Has `.retry_after` (seconds) |
96
+ | `InsufficientFunds` | Account balance too low |
97
+ | `SessionEnded` | Provider ended the session. Has `.reason` |
98
+ | `CdpUnrecoverable` | CDP connection lost permanently |
99
+ | `ConnectionLost` | Relay connection lost after max reconnects |
100
+
101
+ ## Session profile (cookies + storage)
102
+
103
+ `browser.profile` lets you snapshot and restore cookies, `localStorage`, and `sessionStorage` between sessions — without involving the relay or backend. The blob stays in your own storage.
104
+
105
+ ```python
106
+ import json
107
+
108
+ # First session — sign up, then export profile
109
+ async with await client.rent(schedule_id) as browser:
110
+ await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
111
+ # ... perform signup, 2FA ...
112
+ profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])
113
+
114
+ with open("reddit_profile.json", "w") as f:
115
+ json.dump(profile, f)
116
+
117
+ # Next session — restore profile (navigate first, then import storage)
118
+ with open("reddit_profile.json") as f:
119
+ profile = json.load(f)
120
+
121
+ async with await client.rent(schedule_id) as browser:
122
+ # Cookies are domain-scoped — set them before navigation
123
+ await browser.profile.import_(profile)
124
+ await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
125
+ # already logged in
126
+ ```
127
+
128
+ **Notes:**
129
+ - `localStorage`/`sessionStorage` require a document context — navigate to the target origin before calling `import_()`, or call it right after navigation.
130
+ - Cookies (`Network.setCookies`) work before any navigation.
131
+ - Use `domains` to export only relevant cookies and avoid importing third-party trackers.
132
+ - Encrypt the blob before writing to disk if it contains sensitive credentials.
133
+ - `import_()` raises `ValueError` on `schema_version` mismatch (future-proofing).
134
+
135
+ ## CDP Lifecycle
136
+
137
+ The relay maintains the CDP connection to the incognito browser tab. If the connection drops, it automatically reattaches with 1s/2s/4s exponential backoff. Commands during reattach are buffered (FIFO, max 50). If 3 reattach attempts fail, a new fallback tab is created. If that also fails, `cdp_unrecoverable` error is sent.
138
+
139
+ ## Real-signup examples
140
+
141
+ See `examples/SMOKE.md` for full runbook.
142
+
143
+ Quick:
144
+ ```bash
145
+ pip install -e ".[dev]"
146
+ export CEKI_API_KEY=...
147
+ export SCHEDULE_ID=...
148
+ python examples/reddit_signup.py
149
+ ```
150
+
151
+ These are NOT automated tests — they require a live relay, an online provider, and a real IMAP mailbox. Run manually as part of Phase 2 acceptance.
152
+
153
+ ## Human Mode
154
+
155
+ Browser actions can optionally include human-like timing — delays before/after actions and per-character typing with jitter.
156
+
157
+ ```python
158
+ # Default: natural profile (enabled by default)
159
+ browser = await client.rent(schedule_id)
160
+
161
+ # Explicit profile
162
+ browser = await client.rent(schedule_id, human="careful")
163
+
164
+ # Disable humanization
165
+ browser = await client.rent(schedule_id, human=None)
166
+
167
+ # Custom profile dict
168
+ browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})
169
+ ```
170
+
171
+ ### High-level methods
172
+
173
+ ```python
174
+ await browser.navigate("https://example.com")
175
+ await browser.click(100, 200)
176
+ await browser.type("Hello, world!") # Per-char with jitter when human mode on
177
+ await browser.scroll(delta_y=-300)
178
+ img_bytes = await browser.screenshot()
179
+ ```
180
+
181
+ ### Runtime control
182
+
183
+ ```python
184
+ prev = browser.set_human("careful") # Switch profile, returns previous
185
+ browser.set_human(None) # Disable mid-session
186
+ ```
187
+
188
+ ### Environment variables
189
+
190
+ - `CEKI_HUMAN_PROFILE` — Override default profile name (e.g., `careful`)
191
+ - `CEKI_HUMAN_PROFILE_PATH` — Path to custom JSON profile file
192
+ - `CEKI_HUMAN_DISABLE=1` — Disable humanization entirely
193
+
194
+ ## CLI
195
+
196
+ The SDK installs a `ceki` CLI binary on your PATH.
197
+
198
+ ### Install
199
+
200
+ ```bash
201
+ pip install ceki-sdk
202
+ ```
203
+
204
+ ### Environment variables
205
+
206
+ | Variable | Required | Purpose |
207
+ |---|---|---|
208
+ | `CEKI_API_KEY` | yes | Agent token (`ag_...`) |
209
+
210
+ ### Quick start
211
+
212
+ ```bash
213
+ export CEKI_API_KEY=ag_...
214
+
215
+ SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
216
+ SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
217
+ ceki navigate $SID https://example.com
218
+ ceki snapshot $SID -o snap.png
219
+ ceki stop $SID
220
+ ```
221
+
222
+ The CLI persists session state locally — after `rent` it saves the session ID so subsequent commands resume it by SID without re-renting.
223
+
224
+ ### Commands
225
+
226
+ #### Discovery and lifecycle
227
+
228
+ | Command | Description |
229
+ |---|---|
230
+ | `search [--limit N] [--filter K=V]…` | List available browsers |
231
+ | `my-browsers` | List browsers with pre-arranged rent contracts |
232
+ | `rent --schedule ID [--mode incognito\|main] [--fingerprint-from FILE]` | Rent a browser |
233
+ | `sessions [--all] [--limit N] [--json]` | List your sessions |
234
+ | `stop SID` | End a session |
235
+ | `wait SID` | Block until the session ends |
236
+
237
+ #### Browser control
238
+
239
+ | Command | Description |
240
+ |---|---|
241
+ | `navigate SID URL` | Open URL |
242
+ | `click SID X Y` | Click at viewport coordinates |
243
+ | `type SID TEXT [--natural]` | Type text into focused element |
244
+ | `scroll SID X Y DY` | Scroll from (X, Y) by `DY` pixels |
245
+ | `screenshot SID -o FILE [--format png\|jpeg] [--full]` | Save screenshot |
246
+ | `snapshot SID -o FILE` | Screenshot + new chat messages |
247
+ | `switch-tab SID` | Switch active tab |
248
+ | `upload SID --selector CSS --file PATH [--filename NAME]` | Attach file to `<input type="file">` |
249
+
250
+ #### Chat with host
251
+
252
+ | Command | Description |
253
+ |---|---|
254
+ | `chat SID send TEXT` | Send message to host |
255
+ | `chat SID next [--timeout SEC]` | Wait for next host message |
256
+ | `chat SID history [--since TS] [--limit N]` | Fetch chat history |
257
+ | `chat SID send-image --image PATH [--text MSG]` | Send image to host |
258
+
259
+ #### Advanced
260
+
261
+ | Command | Description |
262
+ |---|---|
263
+ | `profile SID export -o FILE [--domains CSV] [--no-session-storage]` | Export cookies / localStorage |
264
+ | `profile SID import -i FILE` | Import previously exported profile |
265
+ | `request-captcha SID [--acceptance SEC] [--completion SEC] [--manual]` | Ask host to solve CAPTCHA |
266
+ | `configure SID [--masking-mode VAL] [--fingerprint VAL]` | Toggle masking / fingerprint |
267
+ | `cdp SID --method METHOD [--params JSON]` | Raw CDP command |
268
+
269
+ ### Output and errors
270
+
271
+ Successful commands write a single JSON line to stdout. Errors go to stderr as `{"error": "...", "code": "..."}`. Pipe stdout through `jq` to chain commands.
272
+
273
+ ### Exit codes
274
+
275
+ | Code | Meaning |
276
+ |---|---|
277
+ | `0` | success |
278
+ | `1` | generic error |
279
+ | `2` | `CEKI_API_KEY` not set |
280
+ | `3` | session not found or not owner |
281
+ | `4` | timeout |
282
+ | `5` | network / connection error |
283
+ | `130` | interrupted (Ctrl-C) |
284
+
285
+ Full reference (with EN+RU): https://browser.ceki.me/docs#cli
286
+
287
+ ## Development
288
+
289
+ ```bash
290
+ pip install -e ".[dev]"
291
+ pytest
292
+ ruff check ceki_sdk/
293
+ mypy ceki_sdk/
294
+ ```
@@ -0,0 +1,265 @@
1
+ # ceki-sdk
2
+
3
+ Python SDK for [browser.ceki.me](https://browser.ceki.me) — rent real browsers from real people for AI agent automation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install ceki-sdk
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ import asyncio
15
+ import os
16
+ from ceki_sdk import connect, ConnectOptions
17
+
18
+ async def main():
19
+ client = await connect(os.environ["CEKI_API_KEY"])
20
+ options = await client.search({"geo": "US", "language": "en"})
21
+ browser = await client.rent(options[0].schedule_id)
22
+ # ... CDP calls (see docs)
23
+ await browser.close()
24
+ await client.close()
25
+
26
+ asyncio.run(main())
27
+ ```
28
+
29
+ **BREAKING in 2.2.0:** `connect()` no longer accepts `relay_url=` or `reconnect=` kwargs — pass a `ConnectOptions` object instead.
30
+
31
+ ## Environment Variables
32
+
33
+ | Variable | Description |
34
+ |---|---|
35
+ | `CEKI_API_KEY` | Your API key (required) |
36
+
37
+ ## API
38
+
39
+ ### `connect(api_key, options: ConnectOptions | None = None) -> Client`
40
+
41
+ Establish a WebSocket connection to the relay. Returns a `Client` instance.
42
+
43
+ ### `ConnectOptions`
44
+
45
+ | Field | Default | Description |
46
+ |---|---|---|
47
+ | `reconnect` | `True` | Auto-reconnect on disconnect |
48
+
49
+ ### `client.search(filters=None, limit=20) -> list[BrowserOption]`
50
+
51
+ Search for available browsers. Filters: `geo`, `language`, etc.
52
+
53
+ ### `client.rent(schedule_id) -> Browser`
54
+
55
+ Rent a browser by schedule ID. Waits up to 60s for a match.
56
+
57
+ ### `client.close()`
58
+
59
+ Close all sessions and the connection.
60
+
61
+ ## Error Codes
62
+
63
+ | Exception | Cause |
64
+ |---|---|
65
+ | `AuthFailed` | Invalid API key or token revoked |
66
+ | `RateLimitExceeded` | Too many requests. Has `.retry_after` (seconds) |
67
+ | `InsufficientFunds` | Account balance too low |
68
+ | `SessionEnded` | Provider ended the session. Has `.reason` |
69
+ | `CdpUnrecoverable` | CDP connection lost permanently |
70
+ | `ConnectionLost` | Relay connection lost after max reconnects |
71
+
72
+ ## Session profile (cookies + storage)
73
+
74
+ `browser.profile` lets you snapshot and restore cookies, `localStorage`, and `sessionStorage` between sessions — without involving the relay or backend. The blob stays in your own storage.
75
+
76
+ ```python
77
+ import json
78
+
79
+ # First session — sign up, then export profile
80
+ async with await client.rent(schedule_id) as browser:
81
+ await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
82
+ # ... perform signup, 2FA ...
83
+ profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])
84
+
85
+ with open("reddit_profile.json", "w") as f:
86
+ json.dump(profile, f)
87
+
88
+ # Next session — restore profile (navigate first, then import storage)
89
+ with open("reddit_profile.json") as f:
90
+ profile = json.load(f)
91
+
92
+ async with await client.rent(schedule_id) as browser:
93
+ # Cookies are domain-scoped — set them before navigation
94
+ await browser.profile.import_(profile)
95
+ await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
96
+ # already logged in
97
+ ```
98
+
99
+ **Notes:**
100
+ - `localStorage`/`sessionStorage` require a document context — navigate to the target origin before calling `import_()`, or call it right after navigation.
101
+ - Cookies (`Network.setCookies`) work before any navigation.
102
+ - Use `domains` to export only relevant cookies and avoid importing third-party trackers.
103
+ - Encrypt the blob before writing to disk if it contains sensitive credentials.
104
+ - `import_()` raises `ValueError` on `schema_version` mismatch (future-proofing).
105
+
106
+ ## CDP Lifecycle
107
+
108
+ The relay maintains the CDP connection to the incognito browser tab. If the connection drops, it automatically reattaches with 1s/2s/4s exponential backoff. Commands during reattach are buffered (FIFO, max 50). If 3 reattach attempts fail, a new fallback tab is created. If that also fails, `cdp_unrecoverable` error is sent.
109
+
110
+ ## Real-signup examples
111
+
112
+ See `examples/SMOKE.md` for full runbook.
113
+
114
+ Quick:
115
+ ```bash
116
+ pip install -e ".[dev]"
117
+ export CEKI_API_KEY=...
118
+ export SCHEDULE_ID=...
119
+ python examples/reddit_signup.py
120
+ ```
121
+
122
+ These are NOT automated tests — they require a live relay, an online provider, and a real IMAP mailbox. Run manually as part of Phase 2 acceptance.
123
+
124
+ ## Human Mode
125
+
126
+ Browser actions can optionally include human-like timing — delays before/after actions and per-character typing with jitter.
127
+
128
+ ```python
129
+ # Default: natural profile (enabled by default)
130
+ browser = await client.rent(schedule_id)
131
+
132
+ # Explicit profile
133
+ browser = await client.rent(schedule_id, human="careful")
134
+
135
+ # Disable humanization
136
+ browser = await client.rent(schedule_id, human=None)
137
+
138
+ # Custom profile dict
139
+ browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})
140
+ ```
141
+
142
+ ### High-level methods
143
+
144
+ ```python
145
+ await browser.navigate("https://example.com")
146
+ await browser.click(100, 200)
147
+ await browser.type("Hello, world!") # Per-char with jitter when human mode on
148
+ await browser.scroll(delta_y=-300)
149
+ img_bytes = await browser.screenshot()
150
+ ```
151
+
152
+ ### Runtime control
153
+
154
+ ```python
155
+ prev = browser.set_human("careful") # Switch profile, returns previous
156
+ browser.set_human(None) # Disable mid-session
157
+ ```
158
+
159
+ ### Environment variables
160
+
161
+ - `CEKI_HUMAN_PROFILE` — Override default profile name (e.g., `careful`)
162
+ - `CEKI_HUMAN_PROFILE_PATH` — Path to custom JSON profile file
163
+ - `CEKI_HUMAN_DISABLE=1` — Disable humanization entirely
164
+
165
+ ## CLI
166
+
167
+ The SDK installs a `ceki` CLI binary on your PATH.
168
+
169
+ ### Install
170
+
171
+ ```bash
172
+ pip install ceki-sdk
173
+ ```
174
+
175
+ ### Environment variables
176
+
177
+ | Variable | Required | Purpose |
178
+ |---|---|---|
179
+ | `CEKI_API_KEY` | yes | Agent token (`ag_...`) |
180
+
181
+ ### Quick start
182
+
183
+ ```bash
184
+ export CEKI_API_KEY=ag_...
185
+
186
+ SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
187
+ SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
188
+ ceki navigate $SID https://example.com
189
+ ceki snapshot $SID -o snap.png
190
+ ceki stop $SID
191
+ ```
192
+
193
+ The CLI persists session state locally — after `rent` it saves the session ID so subsequent commands resume it by SID without re-renting.
194
+
195
+ ### Commands
196
+
197
+ #### Discovery and lifecycle
198
+
199
+ | Command | Description |
200
+ |---|---|
201
+ | `search [--limit N] [--filter K=V]…` | List available browsers |
202
+ | `my-browsers` | List browsers with pre-arranged rent contracts |
203
+ | `rent --schedule ID [--mode incognito\|main] [--fingerprint-from FILE]` | Rent a browser |
204
+ | `sessions [--all] [--limit N] [--json]` | List your sessions |
205
+ | `stop SID` | End a session |
206
+ | `wait SID` | Block until the session ends |
207
+
208
+ #### Browser control
209
+
210
+ | Command | Description |
211
+ |---|---|
212
+ | `navigate SID URL` | Open URL |
213
+ | `click SID X Y` | Click at viewport coordinates |
214
+ | `type SID TEXT [--natural]` | Type text into focused element |
215
+ | `scroll SID X Y DY` | Scroll from (X, Y) by `DY` pixels |
216
+ | `screenshot SID -o FILE [--format png\|jpeg] [--full]` | Save screenshot |
217
+ | `snapshot SID -o FILE` | Screenshot + new chat messages |
218
+ | `switch-tab SID` | Switch active tab |
219
+ | `upload SID --selector CSS --file PATH [--filename NAME]` | Attach file to `<input type="file">` |
220
+
221
+ #### Chat with host
222
+
223
+ | Command | Description |
224
+ |---|---|
225
+ | `chat SID send TEXT` | Send message to host |
226
+ | `chat SID next [--timeout SEC]` | Wait for next host message |
227
+ | `chat SID history [--since TS] [--limit N]` | Fetch chat history |
228
+ | `chat SID send-image --image PATH [--text MSG]` | Send image to host |
229
+
230
+ #### Advanced
231
+
232
+ | Command | Description |
233
+ |---|---|
234
+ | `profile SID export -o FILE [--domains CSV] [--no-session-storage]` | Export cookies / localStorage |
235
+ | `profile SID import -i FILE` | Import previously exported profile |
236
+ | `request-captcha SID [--acceptance SEC] [--completion SEC] [--manual]` | Ask host to solve CAPTCHA |
237
+ | `configure SID [--masking-mode VAL] [--fingerprint VAL]` | Toggle masking / fingerprint |
238
+ | `cdp SID --method METHOD [--params JSON]` | Raw CDP command |
239
+
240
+ ### Output and errors
241
+
242
+ Successful commands write a single JSON line to stdout. Errors go to stderr as `{"error": "...", "code": "..."}`. Pipe stdout through `jq` to chain commands.
243
+
244
+ ### Exit codes
245
+
246
+ | Code | Meaning |
247
+ |---|---|
248
+ | `0` | success |
249
+ | `1` | generic error |
250
+ | `2` | `CEKI_API_KEY` not set |
251
+ | `3` | session not found or not owner |
252
+ | `4` | timeout |
253
+ | `5` | network / connection error |
254
+ | `130` | interrupted (Ctrl-C) |
255
+
256
+ Full reference (with EN+RU): https://browser.ceki.me/docs#cli
257
+
258
+ ## Development
259
+
260
+ ```bash
261
+ pip install -e ".[dev]"
262
+ pytest
263
+ ruff check ceki_sdk/
264
+ mypy ceki_sdk/
265
+ ```