Metadata-Version: 2.4
Name: solari-desktop
Version: 0.1.1
Summary: Python SDK for Solari Desktop — managed, hardware-isolated Linux desktops with a computer-use action API.
Project-URL: Homepage, https://getsolari.com
Author: Solari
License: MIT
Keywords: automation,computer-use,desktop,solari,vnc
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Requires-Dist: websockets>=11.0
Description-Content-Type: text/markdown

# solari-desktop (Python)

Python SDK for **Solari Desktop** — create managed, hardware-isolated Linux
desktops that boot in well under a second, embed the live VNC stream, and drive
them with a **computer-use** action API (exec, fs, mouse/keyboard, screenshot,
clipboard, processes).

This is a faithful port of the TypeScript `@solarisdk/sdk` package; the
class, method, and field names match it one-for-one (snake_case where TS uses
camelCase for method/parameter names; wire/response field names are kept as-is).

```
SDK ──HTTPS──▶ Gateway  (create / get / destroy sessions)
SDK ──WSS────▶ Gateway  (control channel: computer-use JSON-RPC)
            ──WSS────▶  (stream channel: RFB/VNC bytes, embed in a viewer)
```

## Install

```sh
pip install solari-desktop
```

Depends only on [`httpx`](https://www.python-httpx.org/) (HTTP) and
[`websockets`](https://websockets.readthedocs.io/) (control channel). Requires
Python 3.9+.

## Usage

The primary API is **async** (matching the asyncio WebSocket control channel):

```python
import asyncio
from solari_desktop import DesktopClient


async def main() -> None:
    async with DesktopClient(
        api_key="...",
        base_url="https://api.getsolari.com",
    ) as client:
        # Create a session (assigned from the warm pool — typically sub-second).
        desktop = await client.create(
            template="default",
            resolution="1280x720",
            timeout_ms=1_800_000,   # rolling idle window: auto-pause after 30 min idle
        )

        # Embed this URL in a VNC viewer to watch the live desktop.
        print(desktop.streamUrl)

        # Open the control channel, then drive the desktop.
        await desktop.connect()

        shot = await desktop.screenshot(format="png")          # bytes
        out = await desktop.exec("ls", args=["-la", "/home"])
        print(out.exitCode, out.stdout)

        await desktop.mouse.click(640, 360, humanize=True)
        await desktop.keyboard.press(["ctrl", "c"])
        await desktop.fs.write("/tmp/note.txt", "hello")
        text = await desktop.fs.read_text("/tmp/note.txt")

        # Public preview URL for an in-guest port (requires PREVIEW_DOMAIN).
        preview = await desktop.preview_url(3000)   # {"url": ..., "token"?: ...}
        print(preview["url"])

        # Pause (snapshot RAM+disk, free the slot) and resume later.
        await desktop.pause()
        await desktop.resume()

        await desktop.close()
        await client.destroy(desktop.sessionId)


asyncio.run(main())
```

A thin synchronous wrapper, `SyncDesktopClient`, is available for the
session-lifecycle calls (`create`, `get`, `destroy`, `attach`) when you are not
running an event loop. The returned `Desktop` handle remains async.

## Client API (`DesktopClient`)

| Method | Description |
|---|---|
| `DesktopClient(api_key=, base_url=, http=None, call_timeout_ms=None)` | Construct a client. |
| `await create(template="default", ttl_seconds=None, timeout_ms=None, resolution=None, metadata=None, record=None, lifecycle=None)` → `Desktop` | `POST /desktops`. `timeout_ms` sets a rolling idle window; `lifecycle` controls idle behaviour (default auto-pause). |
| `await get(session_id)` | `GET /desktops/:id` → `GetDesktopResponse(sessionId, status, expiresAt)`. |
| `await destroy(session_id)` | `DELETE /desktops/:id` → `DeleteDesktopResponse(ok=True)` (idempotent). |
| `attach(session)` → `Desktop` | Re-create a handle from a saved `CreateDesktopResponse`. |
| `await aclose()` | Close the owned HTTP client. Also usable as an async context manager. |

Gateway errors are mapped to typed errors: `AuthError` (401), `PlanError`
(402), `ConcurrencyLimitError` (429), `NoCapacityError` (503), and a generic
`GatewayError` otherwise — all subclasses of `SolariError`.

## Desktop handle (`Desktop`)

Properties: `sessionId`, `streamUrl`, `controlUrl`, `expiresAt`, `connected`.

`await connect()` opens the control WebSocket; `await close()` tears it down.
Each action sends a `{ id, method, params }` JSON-RPC frame and awaits the
matching `{ id, ok, result }` reply, correlated by `id`, with a per-call
timeout (`TimeoutError`). A failed RPC raises `ActionError`.

### Computer-use actions

| Surface | Methods |
|---|---|
| `await desktop.exec(cmd, args=None, cwd=None, timeout_ms=None)` | → `ExecResult(exitCode, stdout, stderr)` |
| `desktop.fs` | `read(path)→bytes`, `read_text(path)→str`, `write(path, data, mode=None)`, `list(path)→list[FsEntry]` |
| `desktop.mouse` | `move(x,y,humanize=)`, `click(x,y,button=,humanize=)`, `down/up(x,y,button=)`, `scroll(x,y,...)` |
| `desktop.keyboard` | `type(text)`, `press(keys)`, `down(keys)`, `up(keys)` |
| `await desktop.screenshot(format="png", quality=None)` | → `bytes` |
| `desktop.display` | `set(w, h)` |
| `desktop.clipboard` | `get()→str`, `set(text)` |
| `desktop.process` | `list()→list[ProcessInfo]`, `kill(pid)` |
| `await desktop.health()` | → `HealthResult(ready, display, vnc)` |

### Lifecycle & preview

Since the 2026-07 desktop/sandbox VM consolidation, a desktop is backed by the
same unified session record as a sandbox, so the handle carries the full
lifecycle surface (routed to `/sandboxes/:id/*`):

| Method | Description |
|---|---|
| `await desktop.pause()` / `await desktop.resume()` | Pause (snapshot RAM+disk, free the slot) / resume (re-acquires a slot; may raise `ConcurrencyLimitError` if the org is at cap). |
| `await desktop.set_timeout(timeout_ms)` | Extend the rolling idle keep-alive → `{ expiresAt }`. |
| `await desktop.metrics()` / `await desktop.snapshot(name=None)` / `await desktop.revert(id)` | Metrics + in-place snapshot/restore. |
| `await desktop.preview_url(port)` | Public preview URL `{ url, token? }` for an in-guest port. |

See `examples/quickstart.py` for an end-to-end script.

## Deviations from the TypeScript SDK

- **Async-first.** The TS SDK is promise-based; this SDK exposes the same
  surface as `async def` coroutines, plus `SyncDesktopClient` for the
  lifecycle calls. The TS SDK does not split sync/async, so no async/sync split
  is exposed beyond that convenience wrapper.
- **Naming.** Method and parameter names are snake_case (`read_text`,
  `ttl_seconds`, `call_timeout_ms`); constructor args are keyword-only. Wire
  and response field names (`sessionId`, `exitCode`, `streamUrl`, …) are kept
  verbatim so dataclass attributes match the JSON.
- **Bytes vs `Uint8Array`.** `fs.read`/`screenshot` return `bytes`;
  `fs.write` accepts `bytes | str`.
- **HTTP/WS deps.** Uses `httpx` and `websockets` instead of `fetch`/`ws`.
```
