Metadata-Version: 2.4
Name: invilabs
Version: 0.1.11
Summary: Blocking Python SDK for Invi Labs phone automation
Author: Invi Labs
Project-URL: Homepage, https://invilabs.io
Project-URL: Documentation, https://sdk.invilabs.io
Project-URL: Changelog, https://sdk.invilabs.io/downloads/
Keywords: iphone,automation,sdk,ai-agent,ocr,mcp,model-context-protocol
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: mcp
Requires-Dist: Pillow>=9.1; extra == "mcp"

# Invi Labs Python SDK

Automate real iPhones through the Invi Labs API using your API key and phone ID.
Non-jailbroken iPhones require [Invi Dongle](https://invilabs.io/connect).

## Install

```sh
python -m pip install invilabs
```

Requires Python 3.10 or newer. No third-party runtime dependencies.

## Connect and run an action

Set `INVI_API_KEY` and `INVI_PHONE_ID` in your environment. Keep your API key private.

```python
import os
from invilabs import Client

client = Client(os.environ["INVI_API_KEY"])
phone = client.phone(os.environ["INVI_PHONE_ID"])

print(phone.details())
phone.tap(100, 200)
phone.save_screenshot("screen.png")
```

Coordinates use full-resolution screenshot pixels. Calls block until completion;
the SDK waits and retries when the phone reports `device_busy`. A definite
`device_offline` response waits for reconnect with delays of 1, 2, and 3 seconds,
up to three retries (six seconds total by default). Each offline
rejection uses a new operation key because the phone received no input.
Uncertain execution is never automatically repeated; use the operation key
to look up its status. Set `Client(..., offline_retries=0)` to disable reconnect
waiting, or choose an integer from 0 to 30 for longer outages.

## AI generation

```python
result = client.generate(
    "Generate an email address using example.com",
    result_type="email",
)
print(result["data"]["text"])
print(client.balance())
```

Generation supports text, usernames and email address strings, optionally using a
local PNG/JPEG image. It does not create mailboxes. OCR, AI prompts and generation
use your shared Account AI balance. No AI-provider key is required.

## Features

- Tap, swipe, scroll, type, explicit key presses and opening installed apps.
- Screenshots as PNG bytes or saved to your computer.
- Grouped OCR with text and bounding boxes.
- Single-action AI prompts and structured screen answers.
- Image/video import through Invi Helper on supported dongle phones.
- Device details, balance and operation recovery.
- An MCP server (`invilabs-mcp`) so AI agents can drive your phones.

All network operations use the public HTTPS API with your API key. The package
contains client code and response contracts; phone control and AI processing run
on Invi Labs services. API keys remain on your computer and are sent only as an
authentication header to the configured API. HTTP redirects are not followed.

This is an initial release. Device support and feature verification limits are
listed in the [release notes](https://sdk.invilabs.io/downloads/).

[Documentation](https://sdk.invilabs.io/) ·
[Quickstart](https://sdk.invilabs.io/quickstart/) ·
[Generation and outputs](https://sdk.invilabs.io/reference/generation/) ·
[Errors and recovery](https://sdk.invilabs.io/reference/credits-and-recovery/)

## Typing completion

Version 0.1.1 waits for the dongle's final typing acknowledgment. Long typing and
AI actions remain synchronous to Python while the SDK waits for the same server
operation to finish. It never starts a second copy while waiting. Call
`phone.press("enter")` after `phone.type(text)` returns to submit a search.
A lost connection can still produce `ExecutionUncertainError`; inspect the
recorded operation and phone state before deciding on another action.

### Tap text with OCR (0.1.2)

```python
result = phone.tap_text("Add a comment")
result = phone.tap_text(r"^(?:add|write) (?:a )?comment[.…]*$", regex=True)
print(result["point"], result["ocr"]["credits_charged"])
```

Matches the full trimmed OCR block case-insensitively by default; regex uses
`re.search`. The tap lands randomly inside an inset of the block's bounds.
Use `randomize=False` for its center. Multiple matches raise
`AmbiguousTextError` unless a zero-based `occurrence` is supplied; no match raises
`TargetNotFoundError`. Neither error taps. Regex substring matches use the whole
block's box. Keep the screen stable between OCR and tap: these are two operations.

Returns `match` (OCR block), `point` (`x`, `y` pixels), `ocr` (blocks, credit charge,
balance), and `tap` (execution status and coordinates). Only the normal OCR credit
charge applies, including when no match is found. A tap failure retains the OCR
result in `error.details["ocr"]`. This helper makes no VLM request.


### Check text presence with OCR (0.1.2)

```python
check = phone.text_exists("Continue", region="middle")
if check["exists"]:
    print(check["matches"])

check = phone.text_exists(r"^(?:add|write) (?:a )?comment[.…]*$", regex=True, region="bottom")
check = phone.text_exists("Comments", region=(0, 900, 1170, 600))
```

Omit `region` for the full screen. Named regions are full-width horizontal thirds;
custom tuples are `(x,y,width,height)` in original screenshot pixels. A block's
center determines region membership. Literal and regex rules match `tap_text`.
Returns `exists` (bool), `matches` (OCR blocks), `region` (resolved pixel box or
None), and `ocr` (full result with charge and balance). Check `result["exists"]`,
not the dictionary's truthiness. No match is False; OCR failures raise errors.
One billed full-screen OCR request is made regardless of region or match count.
Named regions also read dimensions before and after OCR and reject geometry
changes. This method never taps or calls a VLM.


## Jailbroken-only extensions (0.1.5+)

The `phone.jailbroken` namespace is available only for jailbroken iPhones with
our current daemon, UI tweak and container service. Dongle phones reject these
operations; their existing controls and OCR helpers are unchanged.

```python
jb = phone.jailbroken
print(jb.apps())
print(jb.containers.list("com.example.app"))
created = jb.containers.create("com.example.app", "testing")
jb.open_app("com.example.app", cid=created["cid"])
print(jb.containers.active("com.example.app"))
jb.containers.set_active("com.example.app", "default")
jb.containers.delete("com.example.app", created["cid"])
print(jb.ui_dump())
jb.tap_text("Settings")
```

Containers use the account's existing slot pool and appear in the panel. Container
names contain 1–64 letters, digits, underscores or hyphens. The default container
cannot be deleted. Switching stops the app; `open_app(..., cid=...)` switches and
launches under one lease. A lost acknowledgment is never automatically replayed.

`jb.tap_text` uses exact trimmed native UI text and converts point bounds to pixel
coordinates. Matching is case insensitive by default. Duplicate matches require
`occurrence=0` (or another zero-based occurrence, in screen reading order).
Disabled and offscreen elements are excluded. UI dump targeting is unbilled and
has no OCR fallback. Native dumps require the UI tweak in the foreground app;
some custom-rendered content is unavailable. `phone.tap_text` retains its existing
OCR behavior on all phones.

### Jailbroken phone controls (0.1.9)

```python
jb = phone.jailbroken
jb.tap(375, 667)                      # same as phone.tap
jb.swipe(375, 1100, 375, 300)         # same as phone.swipe
jb.long_press(375, 667, duration_ms=800)
jb.double_tap(375, 667)               # 4.7-inch screens
jb.home()
jb.app_switcher()
print(jb.foreground_app())            # {"bundle_id": ..., "home_screen": ...}
jb.close_app()                        # the app on screen
jb.close_app("com.burbn.instagram")   # one app by bundle ID
print(jb.close_all_apps())            # {"closed": ["Instagram", ...], "failed": 0}
print(jb.screen_state())              # {"screen_on": True, ...}
jb.lock(); jb.wake(); jb.unlock()
```

`close_app` and `close_all_apps` never close system UI, Invi apps, jailbreak
tools or VPN/proxy apps (`app_protected`). `close_app()` on the Home screen
returns `no_app_open`. `unlock()` works on phones without a passcode. The
`jb_*` operations are refused on dongle phones before anything is sent.

Use the common `phone.upload_media(path)` and `phone.prompt(...)` methods on both
phone types. Jailbroken uploads use an exact native Photos transaction and wait
for its asset acknowledgment before deleting the temporary server copy. Image/video limits,
permissions, shared credits for AI prompts and operation recovery remain the same.

## MCP server for AI agents (0.1.11)

Invi Labs hosts a Model Context Protocol server at `https://api.invilabs.io/mcp`.
It lets Claude, Codex, Cursor and other MCP clients control your phones with
your API key, with nothing to install: add it as a remote (HTTP) server and send
the key as `Authorization: Bearer YOUR_API_KEY`.

```sh
claude mcp add --transport http --scope user invilabs https://api.invilabs.io/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"
```

Cursor and most other clients:

```json
{
  "mcpServers": {
    "invilabs": {
      "url": "https://api.invilabs.io/mcp",
      "headers": {"Authorization": "Bearer YOUR_API_KEY"}
    }
  }
}
```

Your agent does the looking and deciding: it reads screenshots with its own
model and chooses where to tap. The server has no OCR or AI of its own and never
spends your AI credits, so `phone_ocr`, `phone_prompt`, `phone_tap_text`,
`phone_text_exists`, `generate` and `balance` are not MCP tools (they remain in
the SDK). Every other SDK method is a tool, generated from the SDK: `client.X` is
`X`, `phone.X` is `phone_X`, `phone.jailbroken.X` is `jb_X` and
`phone.jailbroken.containers.X` is `jb_containers_X`.

Narrow the tools in the URL: `?phone=ID` (repeatable; the first is the default),
`read_only=1`, `tools=phone_*,devices` or `exclude_tools=jb_*`. The hosted server
cannot reach your files, so `phone_upload_media` and `phone_save_screenshot` are
only offered by the local server.

### Run it locally

The same server ships in this package as `invilabs-mcp` (stdio). Claude Code,
with `uv` installed:

```sh
claude mcp add invilabs --scope user --env INVILABS_API_KEY=YOUR_API_KEY \
  -- uvx --from "invilabs[mcp]@latest" invilabs-mcp
```

Other clients use the same command and environment, for example:

```json
{
  "mcpServers": {
    "invilabs": {
      "command": "uvx",
      "args": ["--from", "invilabs[mcp]@latest", "invilabs-mcp"],
      "env": {"INVILABS_API_KEY": "YOUR_API_KEY"}
    }
  }
}
```

`@latest` makes uv check for a new release each time the client starts the
server. The `[mcp]` extra adds Pillow for smaller JPEG screenshots; without it
the server sends PNG. Pass the API key in the client's `env` block, because most
clients do not forward your shell environment (`INVI_API_KEY` also works). GUI
apps such as Claude Desktop may need the full path to `uvx`.

Screenshots are scaled to fit vision models (1170×2532 becomes 725×1568), and
every coordinate the server accepts or returns uses that image's pixels. The
server converts to native pixels for you. Jailbroken `jb_ui_dump` frames come
back in the same pixels.

Real phones take time to react, so the server handles delays for the agent:

- Screen actions (taps, swipes, typing, keys, app launches) accept
  `screenshot_after: true` and `settle_ms` (1000 ms after a tap up to 4000 ms
  after an app launch by default) and return the resulting screen in the same call.
  `phone_open_app` does this by default: on dongle phones a dropped keystroke in
  Spotlight can open a different app.
- A `wait` tool pauses between steps, for example while a video plays.
- Read-only calls such as screenshots are repeated after transient failures
  (`--read-retries`, default 2). Inputs are never repeated automatically.
- When a phone does not confirm an input (`execution_uncertain`), the server
  attaches the current screen and says why: `ack_timeout` (the phone's link
  stalled and the input may still land, so an identical repeat is refused for
  45 s unless `allow_repeat` is set) or `not_confirmed` (the phone most likely
  refused it). A lost response is looked up, so a completed action is reported
  as completed.
- `phone_health` checks a phone before a session: online, busy, whether its
  screen video is streaming, and any unconfirmed action. A phone whose screen
  is not streaming fails fast with `screen_not_streaming`.
- On dongle phones, swipes and scrolls are limited to 3000 ms.

Codex (`~/.codex/config.toml`) needs longer timeouts than its defaults, because
typing and busy phones can take more than a minute:

```toml
[mcp_servers.invilabs]
command = "uvx"
args = ["--from", "invilabs[mcp]@latest", "invilabs-mcp"]
env_vars = ["INVILABS_API_KEY"]
startup_timeout_sec = 60
tool_timeout_sec = 300
```

Options go after `invilabs-mcp`:

| Option | Effect |
| --- | --- |
| `--phone ID` | Only allow this phone (repeatable). The first one is the default phone. |
| `--read-only` | Only tools that change nothing. |
| `--tools "phone_*,devices"` | Only matching tools. `--exclude-tools "jb_*"` hides tools. |
| `--native-coordinates` | Full-resolution screenshots and native pixels. |
| `--image-format png` | PNG instead of JPEG. |
| `--busy-timeout 45` | Seconds to wait for a phone another automation holds (default 45). |
| `--list-tools` | Print the tool definitions and exit. |

`INVILABS_PHONE_ID` sets a default phone without restricting access; with
`--phone` it must be one of the allowed phones. The server tells the model to take a screenshot after each action
and not to repeat an `execution_uncertain` action blindly. Cancelling a tool
call stops the server waiting for a busy or offline phone, so the input is not
sent later. `phone_save_screenshot` only writes `.png` files and never replaces
a file that is not a PNG. Run `invilabs-mcp --help` for every option, including
the screenshot size limits. `invilabs-mcp --http --port 8000` serves the hosted
variant yourself: every request brings its own key, and file tools are off.

## Cloud phone proxy and VPN

SDK 0.1.7 adds connectivity controls for phones with an Invi Labs managed
router. Your API key needs `device.edit_proxy` and access to the phone.

```python
phone = client.phone(phone_id)
print(phone.connectivity(timeout=30))
locations = phone.vpn_locations(timeout=30)
print(locations)

phone.set_proxy("proxy.example", 1080, "username", "password", timeout=60)
phone.use_vpn(timeout=60)  # Restore the currently configured VPN
if locations:
    phone.use_vpn(locations[0], timeout=60)
# Equivalent explicit method:
# phone.set_vpn_location(locations[0], timeout=60)
```

### Server-enforced cooldown

A phone accepts **one network-change attempt every 60 seconds**. The server
shares this cooldown across API keys, server instances, proxy changes, VPN
changes, and managed-proxy changes. Switching keys or endpoints cannot bypass
it. A failed router attempt also consumes the interval. Status and VPN-location
reads remain available during the cooldown.

Requests during the cooldown return HTTP `429`, `error="rate_limited"`, a
`Retry-After` header, and `details.retry_after_seconds`. The SDK raises
`InviError` with these details and does not retry automatically:

```python
from invilabs import InviError

try:
    phone.use_vpn("lax")
except InviError as error:
    if error.code == "rate_limited":
        print("Wait", error.details["retry_after_seconds"], "seconds")
    else:
        raise
```

The optional client `timeout` argument only bounds how long the request waits;
it cannot shorten or disable the server cooldown.

Each call blocks and accepts a positive timeout in seconds (default 60).
A timed-out change raises `ExecutionUncertainError`: the server may still
finish it. Read `phone.connectivity()` before deciding what to do next.
Network changes are never automatically retried, including busy responses.
A timeout is a client waiting limit, not a scheduled switch-back or rollback.

`connectivity()` reports `mode`, `healthy`, `available_vpn_locations`, and
when available `phone_exit_ip`, `proxy_host`, and `proxy_port`. Locations
come from this phone's assigned router; never assume a fixed global list.
Status probes the router's egress and does not prove every app's traffic path.

Proxy support is SOCKS5. Changes affect the entire phone and may interrupt
its connections. Selecting a VPN location removes the active proxy. Switching
to your own proxy or VPN also stops an active managed-proxy subscription's
renewal. SIM-rented phones keep their managed SIM connection; they report
`mode="sim"` with no VPN locations and reject network changes. Customer-owned
phones without a managed router cannot use these controls. Viewer sessions
can remain open, though a network switch can temporarily interrupt them.

No separate owner connectivity helper, router URL, or phone MAC is needed.


## AI credit pricing

AI usage is metered to **0.000001 credit**. Your plan price and monthly credit allowance determine the dollar value of a credit; all plans use the same usage rates.

| Processing | Credits charged |
| --- | ---: |
| 1,000 input tokens | Approximately 0.011111 |
| 1,000 output tokens | Approximately 0.155556 |
| One fresh OCR request | **0.05** |
| Direct taps, swipes, typing, screenshots and media transfers | **0 AI credits** |

The exact token formula is `(input_tokens + 14 * output_tokens) / 90000`, rounded up once per metered invocation to the nearest millionth of a credit. Input includes the system instructions and any image tokens reported by inference. These rates apply to AI prompts, generation and agent model calls on both jailbroken and non-jailbroken phones. Agent flows add the work performed across their model calls and OCR scans.

For example, 2,000 input tokens and 100 output tokens cost **0.037778 credits**. On the $45 / 1,000-credit plan, that is approximately **$0.00170**. A fresh OCR scan costs **$0.00225** on that plan. OCR helpers such as `tap_text()` and `text_exists()` follow the same OCR metering; each fresh scan is charged, while eligible cache hits and operation replays do not charge again.
