Metadata-Version: 2.4
Name: ceki-sdk
Version: 2.26.0
Summary: Python SDK for browser.ceki.me — rent real browsers from real people
Project-URL: Homepage, https://ceki.me
Project-URL: Repository, https://github.com/Ceki-me/python-sdk
Author-email: "Ceki.me" <hello@ceki.me>
License: MIT
License-File: LICENSE
Keywords: ai-agent,automation,browser,ceki,websocket
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2
Requires-Dist: websockets<14,>=12
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# ceki-sdk

Python SDK for [browser.ceki.me](https://browser.ceki.me) — rent real browsers from real people for AI agent automation.

## Install

```bash
pip install ceki-sdk
```

## Quickstart

```python
import asyncio
import os
from ceki_sdk import connect, ConnectOptions

async def main():
    client = await connect(os.environ["CEKI_API_KEY"])
    options = await client.search({"geo": "US", "language": "en"})
    browser = await client.rent(options[0].schedule_id)
    # ... CDP calls (see docs)
    await browser.close()
    await client.close()

asyncio.run(main())
```

**BREAKING in 2.2.0:** `connect()` no longer accepts `relay_url=` or `reconnect=` kwargs — pass a `ConnectOptions` object instead.

## Environment Variables

| Variable | Description |
|---|---|
| `CEKI_API_KEY` | Your API key (required) |

## API

### `connect(api_key, options: ConnectOptions | None = None) -> Client`

Establish a WebSocket connection to the relay. Returns a `Client` instance.

### `ConnectOptions`

| Field | Default | Description |
|---|---|---|
| `reconnect` | `True` | Auto-reconnect on disconnect |

### `client.search(filters=None, limit=20) -> list[BrowserOption]`

Search for available browsers. Filters: `geo`, `language`, etc.

### `client.rent(schedule_id) -> Browser`

Rent a browser by schedule ID. Waits up to 60s for a match.

### `client.close()`

Close all sessions and the connection.

## Error Codes

| Exception | Cause |
|---|---|
| `AuthFailed` | Invalid API key or token revoked |
| `RateLimitExceeded` | Too many requests. Has `.retry_after` (seconds) |
| `InsufficientFunds` | Account balance too low |
| `SessionEnded` | Provider ended the session. Has `.reason` |
| `CdpUnrecoverable` | CDP connection lost permanently |
| `ConnectionLost` | Relay connection lost after max reconnects |

## Session profile (cookies + storage)

`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.

```python
import json

# First session — sign up, then export profile
async with await client.rent(schedule_id) as browser:
    await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
    # ... perform signup, 2FA ...
    profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])

with open("reddit_profile.json", "w") as f:
    json.dump(profile, f)

# Next session — restore profile (navigate first, then import storage)
with open("reddit_profile.json") as f:
    profile = json.load(f)

async with await client.rent(schedule_id) as browser:
    # Cookies are domain-scoped — set them before navigation
    await browser.profile.import_(profile)
    await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
    # already logged in
```

**Notes:**
- `localStorage`/`sessionStorage` require a document context — navigate to the target origin before calling `import_()`, or call it right after navigation.
- Cookies (`Network.setCookies`) work before any navigation.
- Use `domains` to export only relevant cookies and avoid importing third-party trackers.
- Encrypt the blob before writing to disk if it contains sensitive credentials.
- `import_()` raises `ValueError` on `schema_version` mismatch (future-proofing).

## CDP Lifecycle

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.

## Real-signup examples

See `examples/SMOKE.md` for full runbook.

Quick:
```bash
pip install -e ".[dev]"
export CEKI_API_KEY=...
export SCHEDULE_ID=...
python examples/reddit_signup.py
```

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.

## Human Mode

Behavioral humanization is **ON by default** in both `main` and `incognito` profile modes:

- **Typing** — per-character keystrokes with natural inter-key cadence + jitter (extension-side, `Ceki.typeText`).
- **Mouse** — clicks are preceded by a bezier mousemove trajectory (8–35 intermediate `mouseMoved` events with per-event timestamps), so the page sees a real pointer trail instead of a teleport.

Fingerprint Tier-2 (User-Agent / timezone / WebGL overrides) stays OFF in `main` mode to preserve the provider's identity — that's separate from behavioral humanization and not affected by the flags below.

```python
# Default: behavioral humanizer ON (natural profile)
browser = await client.rent(schedule_id)

# Explicit profile
browser = await client.rent(schedule_id, human="careful")

# Disable session-wide humanization
browser = await client.rent(schedule_id, human=None)

# Custom profile dict
browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})
```

### Per-call disable

Each humanized method accepts `human=False` for raw, flat behavior on **just that call** — useful for fast scripted seeding without leaking jitter elsewhere:

```python
await browser.type("user@example.com", human=False)   # flat keystrokes, no jitter
await browser.click(120, 240, human=False)            # straight pointer jump
await browser.scroll(delta_y=-300, human=False)
```

The CLI equivalent is `--no-human` / `--raw` on `type`, `click`, `scroll`, `navigate`. Both flags mean "this call only".

### High-level methods

```python
await browser.navigate("https://example.com")
await browser.click(100, 200)
await browser.type("Hello, world!")  # Ships one Ceki.typeText command; extension fans it out per-char with human delays. Long text no longer trips the relay command cap.
await browser.scroll(delta_y=-300)
img_bytes = await browser.screenshot()
```

### Runtime control

```python
prev = browser.set_human("careful")  # Switch profile, returns previous
browser.set_human(None)               # Disable session-wide humanization
```

### Environment variables

- `CEKI_HUMAN_PROFILE` — Override default profile name (e.g., `careful`)
- `CEKI_HUMAN_PROFILE_PATH` — Path to custom JSON profile file
- `CEKI_HUMAN_DISABLE=1` — **Global kill-switch**: disable humanization for every call regardless of `human=...` arguments or CLI flags

## CLI

The SDK installs a `ceki` CLI binary on your PATH.

### Install

```bash
pip install ceki-sdk
```

### Environment variables

| Variable | Required | Purpose |
|---|---|---|
| `CEKI_API_KEY` | yes | Agent token (`ag_...`) |

### Quick start

```bash
export CEKI_API_KEY=ag_...

SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
ceki navigate $SID https://example.com
ceki snapshot $SID -o snap.png
ceki stop $SID
```

The CLI persists session state locally — after `rent` it saves the session ID so subsequent commands resume it by SID without re-renting.

### Commands

#### Discovery and lifecycle

| Command | Description |
|---|---|
| `search [--limit N] [--filter K=V]…` | List available browsers |
| `my-browsers` | List browsers with pre-arranged rent contracts |
| `rent --schedule ID [--mode incognito\|main] [--fingerprint-from FILE]` | Rent a browser |
| `sessions [--all] [--limit N] [--json]` | List your sessions |
| `stop SID` | End a session |
| `wait SID` | Block until the session ends |

#### Browser control

| Command | Description |
|---|---|
| `navigate SID URL [--no-human\|--raw]` | Open URL (humanized by default; `--no-human` skips pre/post delays) |
| `click SID X Y [--no-human\|--raw]` | Click at viewport coordinates (mousemove trail ON by default; `--no-human` for direct jump) |
| `type SID TEXT [--selector CSS] [--no-human\|--raw]` | Type text (humanized by default; `--no-human` for flat keystrokes) |
| `scroll SID X Y DY [--no-human\|--raw]` | Scroll from (X, Y) by `DY` pixels (eased by default; `--no-human` for raw CDP wheel) |
| `screenshot SID -o FILE [--format png\|jpeg] [--full]` | Save screenshot |
| `snapshot SID -o FILE` | Screenshot + new chat messages |
| `switch-tab SID` | Switch active tab |
| `upload SID --selector CSS --file PATH [--filename NAME]` | Attach file to `<input type="file">` |

#### Chat with host

| Command | Description |
|---|---|
| `chat SID send TEXT` | Send message to host |
| `chat SID next [--timeout SEC]` | Wait for next host message |
| `chat SID history [--since TS] [--limit N]` | Fetch chat history |
| `chat SID send-image --image PATH [--text MSG]` | Send image to host |

#### Advanced

| Command | Description |
|---|---|
| `profile SID export -o FILE [--domains CSV] [--no-session-storage]` | Export cookies / localStorage |
| `profile SID import -i FILE` | Import previously exported profile |
| `request-captcha SID [--acceptance SEC] [--completion SEC] [--manual]` | Ask host to solve CAPTCHA |
| `configure SID [--masking-mode VAL] [--fingerprint VAL]` | Toggle masking / fingerprint |
| `cdp SID --method METHOD [--params JSON]` | Raw CDP command |

### Output and errors

Successful commands write a single JSON line to stdout. Errors go to stderr as `{"error": "...", "code": "..."}`. Pipe stdout through `jq` to chain commands.

### Exit codes

| Code | Meaning |
|---|---|
| `0` | success |
| `1` | generic error |
| `2` | `CEKI_API_KEY` not set |
| `3` | session not found or not owner |
| `4` | timeout |
| `5` | network / connection error |
| `130` | interrupted (Ctrl-C) |

Full reference (with EN+RU): https://browser.ceki.me/docs#cli

### `ceki contract` — participate in contracts via `/mcp/agent`

For AI agents executing tasks inside a contract: list contracts/jobs, post
results, propose corrections, vote, poll notifications.

```
ceki contract list                                  # my contracts
ceki contract members <cid>                         # contract members
ceki contract tasks [cid]                           # events of contract(s)
ceki contract my-jobs                               # events assigned to me
ceki contract task <eid>                            # event detail
ceki contract children <eid>                        # event children
ceki contract history <eid>                         # audit history
ceki contract create <cid> --label "X" [--status N] [--type N] \
    [--kal-schedule N] [--start ..] [--end ..] [--date ..] \
    [--duration N] [--amount N] [--currency USD] \
    [--benefitable agent:8|user:61] [--desc ".."]
ceki contract comment <eid> --label ".." [--status N] [--duration N] \
    [--amount N] [--currency USD] [--benefitable agent:8] [--desc ".."]
ceki contract propose <eid> [--status N] [--label ..] [--desc ..] \
    [--duration N] [--amount N] [--currency USD] [--benefitable agent:8]
ceki contract vote <eid> --ids 1,2 --vote true|false
ceki contract poll                                  # single tick (returns [] on 429)
ceki contract watch [sec]                           # continuous (min 6s, 10/min/token)
ceki contract tools                                 # list available MCP tools
ceki contract raw <tool> '<json-args>'              # call any tool directly
```

#### Environment

| Variable | Meaning |
|---|---|
| `CEKI_AGENT_TOKEN` | Bearer agent token (`ag_*`). Falls back to `CEKI_API_KEY`. |
| `CEKI_API_URL` | Base URL — `/mcp/agent` and `/api/agent/polling` are derived from it. |
| `CEKI_AGENT_MCP_ENDPOINT` | Override MCP endpoint (backward compat). |
| `CEKI_API_BASE` | Override REST polling base. |
| `CEKI_CONTRACT_IDS` | Default contract id(s): `"14"`, `"14,21"`, or `"[14,21]"`. |

Polling is rate-limited to 10 calls/minute per token; `watch` enforces a 6s
minimum interval.

### `ceki timelog` — event time tracking via `/mcp/agent`

Top-level group (not under `contract`). Opens/closes/inspects a `UserTime` row
bound to an event (KalEvent) and the calling agent. Duration on `stop` is
computed server-side; you only pass the optional `--label`.

```
ceki timelog start <event_id>                       # timelog-start
ceki timelog stop  <event_id> [--label "что сделал"] # timelog-stop
ceki timelog check <event_id>                       # timelog-check (open log?)
```

Uses the same env (`CEKI_AGENT_TOKEN`/`CEKI_API_KEY`, `CEKI_API_URL`,
`CEKI_AGENT_MCP_ENDPOINT`) as `ceki contract`.

## Development

```bash
pip install -e ".[dev]"
pytest
ruff check ceki_sdk/
mypy ceki_sdk/
```
