# Python SDK & CLI

> The official flaxcloud Python client and flax command-line tool: install, authenticate, run commands, manage files, sessions, snapshots, forks, custom images, and the browser.

`flaxcloud` is the official Python client and `flax` command-line tool for Flax Cloud. It wraps
the [HTTP API](./api.md) so you can drive sandboxes from scripts, agents, and your terminal.

## Install

```bash
pip install flaxcloud
```

## Authenticate

Get an API key from the dashboard, then either set an environment variable or log in with the
CLI (which stores the key in `~/.config/flax/config.json`, mode `600`):

```bash
export FLAX_API_KEY=flax_live_...
# or
flax login
```

Credentials resolve in this order: explicit argument / `--key` flag -> `$FLAX_API_KEY` ->
`~/.config/flax/config.json`. The base URL is explicit argument, CLI `--url`, `$FLAX_BASE_URL`, or
`https://flaxcloud.com`.

## SDK quickstart

```python
from flaxcloud import FlaxClient

flax = FlaxClient()  # reads FLAX_API_KEY

with flax.create_sandbox(template="python", name="ci-run") as sb:   # auto-destroys on exit
    print(sb.name, sb.id)
    print(sb.run("python3 -c 'print(6*7)'").stdout)  # "42\n"

    sb.upload("/workspace/app.py", "print('hello')\n")
    print(sb.run("python3 app.py").stdout)
```

The client retries transient network/5xx errors on idempotent calls, sends a versioned
`User-Agent`, and ships type hints (`py.typed`). Configure with
`FlaxClient(api_key=..., base_url=..., timeout=60, max_retries=2)`.

Sandbox names are optional labels for humans. The immutable `sb.id` is still the value to store for
API calls, logs, and support.

### Memory limits

Sandboxes default to `512 MB`. Pass `memory_mb` when a task needs more RAM:

```python
sb = flax.create_sandbox(template="python", memory_mb=1024)  # 1 GB
```

Per-sandbox memory is limited by plan: Free `1 GB`, Pro `2 GB`, Builder `4 GB`, Team `8 GB`.
If you request more than your plan allows, the API rejects the create with `memory_limit` instead
of silently creating a smaller sandbox:

```python
from flaxcloud import FlaxClient, FlaxQuotaError

flax = FlaxClient()

try:
    flax.create_sandbox(template="python", memory_mb=2048)
except FlaxQuotaError as exc:
    if exc.code == "memory_limit":
        print("Choose a smaller sandbox or upgrade the plan.")
    else:
        raise
```

The CLI uses the same limit:

```bash
flax sandbox create --template python --memory 1024
```

### Environment variables

```python
# Sandbox-wide defaults (injected into every command, terminal, and startup process):
sb = flax.create_sandbox(template="python", env={"API_BASE": "https://example.com"})

# Per-command env (merged over and overriding the sandbox defaults):
sb.run("printenv TOKEN", env={"TOKEN": "secret"})
```

### Run code (not just shell)

```python
out = sb.code_run("print(6*7)", language="python")   # also: node/javascript, bash, ruby
print(out.stdout)
```

`code_run` base64-encodes and pipes your snippet to the interpreter, so multi-line code and
arbitrary quotes are safe.

### Git

```python
sb.git.clone("https://github.com/me/repo.git", "/workspace/repo")
sb.git.checkout("feature", "/workspace/repo", create=True)
sb.git.add("/workspace/repo")
sb.git.commit("wip", "/workspace/repo")
sb.git.push("/workspace/repo")
```

(The `python`/`node` templates include git.)

### Commands

```python
out = sb.run("ls -la")            # synchronous: blocks until done
print(out.stdout, out.exit_code)

job = sb.run("npm install", background=True)   # long-running
done = sb.wait(job, timeout=600)               # poll until terminal
print(done.status)                             # "completed" | "failed" | "timeout"
```

### Stateful sessions

A **session** keeps your working directory and exported environment across `run()` calls - so
sequential, dependent commands (`cd`, `export`, `source`, install, run) build on each other:

```python
s = sb.create_session()
s.run("cd /workspace && export TOKEN=abc")
print(s.run("pwd").stdout)          # /workspace
print(s.run("echo $TOKEN").stdout)  # abc
s.delete()

# or as a context manager (auto-deletes):
with sb.create_session() as s:
    s.run("cd src && export ENV=prod")
    print(s.run("pwd").stdout)
```

Each call returns a `SessionResult` with `.stdout`, `.stderr`, `.exit_code`, `.cwd`, and `.ok`.

> Sessions are durable and scale horizontally: rather than holding a live shell process, Flax
> persists the working directory + environment and re-applies them on each command. This means
> **in-session background processes, aliases, and shell functions do not persist** across calls
> (working directory and exported environment do). For a long-running server, use a
> [startup command](#startup-command--previews); for a true live shell, use the terminal.

### Streaming output

Stream combined stdout/stderr **live** as a command runs, instead of waiting for it to finish:

```python
stream = sb.run_stream("for i in 1 2 3; do echo $i; sleep 1; done")
for chunk in stream:
    print(chunk, end="", flush=True)
print("exit:", stream.exit_code)   # set once the stream ends
```

Streamed commands are metered like any other. Async works the same way with `async for`.

### Custom images (templates)

Need your own dependencies or language version? Register a custom image once and create sandboxes
from it. Any public registry image is supported:

```python
# Pull a registry image (blocks until ready by default, then pins it by digest):
tpl = flax.create_template("my-python", image="python:3.12-slim")
print(tpl.status)   # "ready"

sb = flax.create_sandbox(template="my-python")   # by name (or tpl.id)
print(sb.run("python3 --version").stdout)

# Manage templates:
for t in flax.list_templates():     # includes built-ins (blank/python/node)
    print(t.name, t.source_type, t.status)
flax.delete_template(tpl.id)
```

`create_template` waits for the pull to finish; pass `wait=False` to return immediately and poll
with `flax.get_template(id)` (or `flax.wait_for_template(id)`) yourself. Per-plan limits apply
(`max_custom_templates`, `max_image_size_mb`).

Watch the pull happen with `flax.build_logs(id)` (it streams registry pulls and Dockerfile builds
alike), or read the captured log afterward from `flax.get_template(id).build_log`:

```python
tpl = flax.create_template("my-python", image="python:3.12-slim", wait=False)
for line in flax.build_logs(tpl.id):   # layer milestones, pinned digest, size
    print(line, end="")
```

Build from a Dockerfile (when builds are enabled on the server) and stream the build log live:

```python
df = "FROM python:3.12-slim\nRUN pip install pandas numpy"
tpl = flax.create_template("data-img", dockerfile=df, wait=False)
for line in flax.build_logs(tpl.id):   # live build output
    print(line, end="")
tpl = flax.get_template(tpl.id)        # status is now "ready" or "failed"
```

Identical Dockerfiles reuse a cached image. Async mirrors all of this
(`await flax.create_template(...)`, `async for line in flax.build_logs(id)`).

#### Declarative image builder

Instead of hand-writing a Dockerfile, describe the image with `FlaxImage` and let the SDK generate
one. It uses the same Dockerfile build flow under the hood (build status, logs, and `wait` are
identical), so there is no separate build path:

```python
from flaxcloud import FlaxImage

image = (
    FlaxImage.python("3.12")
    .apt_install(["git", "ffmpeg"])
    .pip_install(["requests", "beautifulsoup4"])
    .env({"PYTHONUNBUFFERED": "1"})
    .workdir("/workspace")
)
print(image.to_dockerfile())   # inspect the generated Dockerfile if you like

tpl = flax.create_template_from_image_definition("scraper-agent", image, wait=True)
sb = flax.create_sandbox(template="scraper-agent")
```

Constructors: `base(image)`, `python(version)`, `node(version)`, `debian_slim()`. Chainable steps:
`apt_install`, `pip_install`, `npm_install`, `run`, `env`, `workdir`, `copy_file`, `copy_dir`. Each
package/argument is single-quoted and validated, so version specifiers like `"requests>=2,<3"` are
safe; use `.run(...)` for anything custom. `copy_file`/`copy_dir` need a local build context, which
the hosted builder does not accept yet, so use `.run(...)` (curl/git) to fetch files for now. The
async client has `await flax.create_template_from_image_definition(...)`.

### Filesystem snapshots

Create a reusable snapshot from a sandbox, then start new sandboxes from it:

```python
sb = flax.create_sandbox(
    template="python",
    env={"API_BASE": "https://example.com"},
    startup_command="python3 -m http.server 8000 --bind 0.0.0.0",
)
sb.upload("/workspace/app.py", "print('ready')\n")

snap = sb.create_snapshot("agent-base")
sb.destroy()

restored = flax.create_sandbox(snapshot_id=snap.id)
print(restored.read_text("/workspace/app.py"))

for s in flax.list_snapshots():
    print(s.id, s.name, s.size_bytes)

flax.delete_snapshot(snap.id)
```

Snapshots preserve `/workspace` and sandbox configuration. They do not preserve running processes
or RAM. The async client mirrors the same surface: `await sb.create_snapshot()`,
`await flax.list_snapshots()`, `await flax.create_sandbox(snapshot_id=...)`.

### Forking

Create an independent sandbox copy from a live or stopped source sandbox:

```python
sb = flax.create_sandbox(template="python", env={"CASE": "base"})
sb.upload("/workspace/input.txt", "baseline")

fork = sb.fork(metadata={"branch": "eval-a"})
fork.upload("/workspace/input.txt", "changed in fork")

print(sb.read_text("/workspace/input.txt"))    # baseline
print(fork.read_text("/workspace/input.txt"))  # changed in fork
```

Forks copy `/workspace` plus sandbox configuration. They do not copy running processes, RAM,
terminal sessions, preview links, or command history. The async client mirrors the same surface:
`await sb.fork()` and `await flax.fork_sandbox(sb.id)`.

### Browser capabilities

Create a browser-ready sandbox with capabilities:

```python
sb = flax.create_sandbox(capabilities=["browser"])
print(sb.capabilities)  # ["browser"]

sb.browser.start()
cdp_url = sb.browser.get_cdp_url()
shot = sb.browser.screenshot_bytes()
```

Browser artifacts persist under `/workspace/.flax/browser/`. Browser automation is CDP-first:
connect Playwright, Puppeteer, browser-use, Stagehand, or your own CDP client to the short-lived
authenticated URL from `get_cdp_url()`.

The CLI exposes the same lifecycle:

```bash
flax sandbox create --browser
flax browser start sbx_browser
flax browser cdp-url sbx_browser
flax browser screenshot sbx_browser -o shot.png
flax browser stop sbx_browser
```

### Files

```python
sb.upload("/workspace/data.json", b"{...}")
content = sb.download("/workspace/data.json")
print(sb.read_text("/workspace/data.json"))
for entry in sb.list_files("/workspace"):
    print(entry.name, entry.is_dir, entry.size_bytes)
sb.mkdir("/workspace/out")
sb.move("/workspace/data.json", "/workspace/out/data.json")
print(sb.exists("/workspace/out/data.json"))
print(sb.find("/workspace", name="*.json"))
sb.delete_file("/workspace/out/data.json")
```

### Account & usage

```python
print(flax.me())            # {id, email, plan}
print(flax.usage())         # current usage + plan limits
key = flax.create_api_key("ci")   # full key in key["key"] (shown once)
flax.list_api_keys()
flax.delete_api_key(key["id"])
```

### Async

```python
import asyncio
from flaxcloud import AsyncFlaxClient

async def main():
    async with AsyncFlaxClient() as flax:
        async with await flax.create_sandbox(capabilities=["browser"]) as sb:
            await sb.browser.start()
            print(await sb.browser.get_cdp_url())
            async with await sb.create_session() as s:      # sessions work too
                print((await s.run("pwd")).stdout)
            async for chunk in sb.run_stream("echo streamed"):  # streaming too
                print(chunk, end="")

asyncio.run(main())
```

### Startup command & previews

```python
sb.set_startup("python3 -m http.server 8000 --bind 0.0.0.0")  # auto-runs on resume
sb.run_startup()
print(sb.create_preview_link(8000).url)   # shareable URL (shown once)
```

### Lifecycle & errors

```python
sb.stop(); sb.resume(); sb.destroy()

from flaxcloud import FlaxNotFoundError, FlaxQuotaError, FlaxError
try:
    flax.get_sandbox("sbx_missing")
except FlaxNotFoundError:
    ...
```

All errors subclass `FlaxError` and carry `.code` and `.status`.

## CLI

```bash
flax login                              # save & verify your key
flax whoami

flax sandbox create --template python --startup "python3 -m http.server 8000 --bind 0.0.0.0"
flax sandbox ls
flax sandbox stop   sbx_abc123
flax sandbox resume sbx_abc123
flax sandbox rm     sbx_abc123

flax run  sbx_abc123 "python3 -c 'print(6*7)'"
flax run  sbx_abc123 "npm install" --background      # prints a command id
flax run  sbx_abc123 "make build" --stream           # stream output live
flax logs sbx_abc123 cmd_xyz                         # output + status

flax session create sbx_abc123                       # prints a session id
flax session exec   ses_xyz "cd src && export E=1"   # cwd/env persist
flax session exec   ses_xyz "pwd"
flax session ls     sbx_abc123
flax session rm     ses_xyz

flax template create my-python --image python:3.12-slim   # registry image (streams the pull log)
flax template create data-img --dockerfile ./Dockerfile   # build (streams the build log)
flax template ls
flax template get tpl_abc123
flax template rm  tpl_abc123
flax sandbox create --template my-python              # use it

flax cp ./app.py sbx_abc123:/workspace/app.py        # upload
flax cp sbx_abc123:/workspace/out.txt ./out.txt      # download
flax ls sbx_abc123 /workspace

flax snapshot create sbx_abc123 agent-base           # capture /workspace + config
flax snapshot ls
flax sandbox rm sbx_abc123
flax sandbox create --template python                # or restore with the SDK/API using snapshot_id
flax snapshot rm snap_abc123

flax startup sbx_abc123 --set "node server.js" --run
flax preview sbx_abc123 8000                         # prints a shareable URL
```

`flax --help` (and `flax <command> --help`) lists everything.

> Prefer JavaScript or TypeScript? The [`@flaxcloud/sdk`](./sdk-js.md) package mirrors this
> surface with camelCase names and Promises.
