Metadata-Version: 2.4
Name: cmdop
Version: 0.1.23
Summary: Python SDK for CMDOP agent interaction
Project-URL: Homepage, https://cmdop.com
Project-URL: Documentation, https://cmdop.com
Project-URL: Repository, https://github.com/markolofsen/cmdop-client
Author: CMDOP Team
License: MIT
License-File: LICENSE
Keywords: agent,automation,cmdop,terminal
Classifier: Development Status :: 3 - Alpha
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
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: beautifulsoup4>=4.12.0
Requires-Dist: grpcio>=1.60.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: lxml>=5.0.0
Requires-Dist: protobuf>=4.25.0
Requires-Dist: pydantic-settings>=2.0.0
Requires-Dist: pydantic>=2.5.0
Requires-Dist: rich>=13.0.0
Requires-Dist: toon-python>=0.1.2
Provides-Extra: dev
Requires-Dist: beautifulsoup4>=4.12.0; extra == 'dev'
Requires-Dist: grpcio-tools>=1.60.0; extra == 'dev'
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest-grpc-aio>=0.3.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# cmdop

**Any machine. One API.**

```python
from cmdop import CMDOPClient

with CMDOPClient.remote(api_key="cmd_xxx") as server:
    server.terminal.execute("docker restart app")
    server.files.write("/etc/nginx/nginx.conf", new_config)
    logs = server.files.read("/var/log/app.log")
```

No SSH. No VPN. No open ports.

---

## How

```
Your Code ──── Cloud Relay ──── Agent (on server)
                    │
        Outbound only, works through any NAT/firewall
```

Agent connects out. Your code connects to relay. Done.

---

## Install

```bash
pip install cmdop
```

```python
from cmdop import CMDOPClient, AsyncCMDOPClient

# Remote (via cloud relay)
with CMDOPClient.remote(api_key="cmd_xxx") as client:
    client.files.list("/home")

# Local (direct IPC)
with CMDOPClient.local() as client:
    client.terminal.execute("ls -la")

# Async
async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
    await client.files.read("/etc/hostname")
```

---

## Terminal

```python
session = server.terminal.create()
server.terminal.send_input(session.session_id, "kubectl get pods\n")
output = server.terminal.get_history(session.session_id)
```

| Method | Description |
|--------|-------------|
| `create(shell)` | Start session |
| `send_input(id, data)` | Send commands |
| `get_history(id)` | Get output |
| `resize(id, cols, rows)` | Resize |
| `send_signal(id, signal)` | SIGINT/SIGTERM |
| `close(id)` | End session |

## Files

```python
server.files.list("/var/log")
server.files.read("/etc/nginx/nginx.conf")
server.files.write("/tmp/config.json", b'{"key": "value"}')
```

| Method | Description |
|--------|-------------|
| `list(path)` | List dir |
| `read(path)` | Read file |
| `write(path, content)` | Write file |
| `delete(path)` | Delete |
| `copy/move(src, dst)` | Copy/Move |
| `mkdir(path)` | Create dir |
| `info(path)` | Metadata |

## Agent

```python
from pydantic import BaseModel

class Health(BaseModel):
    status: str
    cpu: float
    issues: list[str]

result = server.agent.run("Check server health", output_schema=Health)
health: Health = result.output  # Typed!
```

---

## Browser

Capability-based API for browser automation.

```python
from cmdop.services.browser.models import WaitUntil

with client.browser.create_session() as s:
    s.navigate("https://shop.com/products", wait_until=WaitUntil.NETWORKIDLE)
    s.dom.close_modal()  # Close popups

    # BeautifulSoup parsing
    soup = s.dom.soup()  # SoupWrapper with chainable API
    for item in soup.select(".product"):
        title = item.select_one("h2").text()
        price = item.attr("data-price")

    # Scrolling with random delays
    for _ in range(10):
        soup = s.dom.soup(".listings")
        s.scroll.js("down", 700)
        s.timing.random(0.8, 1.5)

    # Click with cursor movement
    s.click("button.buy", move_cursor=True)

    # Click all "See more" buttons
    s.input.click_all("See more")

    # Mouse operations
    s.input.mouse_move(500, 300)
    s.input.hover(".tooltip-trigger")

    # JS fetch (bypass CORS, inherit cookies)
    data = s.fetch.json("/api/items")
```

### Core Methods (on session)

| Method | Description |
|--------|-------------|
| `navigate(url, wait_until)` | Go to URL (wait_until: LOAD, DOMCONTENTLOADED, NETWORKIDLE, COMMIT) |
| `click(selector, move_cursor)` | Click element |
| `type(selector, text)` | Type text |
| `wait_for(selector)` | Wait for element |
| `execute_script(js)` | Run JavaScript |
| `screenshot()` | PNG bytes |
| `get_state()` | URL + title |
| `get_page_info()` | Full page info |
| `get/set_cookies()` | Cookie management |

### Capabilities

**`session.scroll`** - Scrolling
| Method | Description |
|--------|-------------|
| `js(dir, amount)` | JS scroll (works on complex sites) |
| `native(dir, amount)` | Browser API scroll |
| `to_bottom()` | Scroll to page bottom |
| `to_element(selector)` | Scroll element into view |
| `info()` | Get scroll position |
| `infinite(extract_fn)` | Smart infinite scroll with extraction |

**`session.input`** - Input operations
| Method | Description |
|--------|-------------|
| `click_js(selector)` | JS click (reliable) |
| `click_all(text, role)` | Click all matching elements |
| `key(key, selector)` | Press keyboard key |
| `hover(selector)` | Hover over element (native) |
| `hover_js(selector)` | Hover via JS |
| `mouse_move(x, y)` | Move cursor to coordinates |

**`session.timing`** - Delays
| Method | Description |
|--------|-------------|
| `wait(ms)` | Wait milliseconds |
| `seconds(n)` | Wait seconds |
| `random(min, max)` | Random delay |
| `timeout(fn, sec, cleanup)` | Run with timeout |

**`session.dom`** - DOM operations
| Method | Description |
|--------|-------------|
| `html(selector)` | Get HTML |
| `text(selector)` | Get text content |
| `soup(selector)` | → SoupWrapper |
| `parse(html)` | → BeautifulSoup |
| `extract(selector, attr)` | Get text/attr list |
| `select(selector, value)` | Dropdown select |
| `close_modal()` | Close dialogs |

**`session.fetch`** - HTTP from browser context
| Method | Description |
|--------|-------------|
| `json(url)` | Fetch JSON |
| `all(requests)` | Parallel fetch |
| `execute(js_code)` | Custom JS fetch code |

**`session.network`** - Network capture (v2.19.0)
| Method | Description |
|--------|-------------|
| `enable(max_exchanges)` | Start capturing HTTP traffic |
| `disable()` | Stop capturing |
| `get_all()` | Get all captured exchanges |
| `filter(url_pattern, methods, status_codes)` | Filter exchanges |
| `last(url_pattern)` | Get most recent matching exchange |
| `api_calls(url_pattern)` | Get XHR/Fetch calls matching pattern |
| `last_json(url_pattern)` | Get JSON body from last matching response |
| `wait_for(url_pattern, timeout_ms)` | Wait for matching request |
| `stats()` | Capture statistics |
| `export_har()` | Export to HAR format |
| `clear()` | Clear captured data |

```python
# Example: Intercept API responses
from cmdop.services.browser.models import WaitUntil

with client.browser.create_session() as s:
    s.network.enable()
    s.navigate("https://app.example.com", wait_until=WaitUntil.NETWORKIDLE)

    # Get last API response
    api = s.network.last("/api/data")
    data = api.json_body()

    # Filter by criteria
    posts = s.network.filter(
        url_pattern="/api/posts",
        methods=["GET"],
        status_codes=[200],
    )

    s.network.disable()
```

## SDKBaseModel

Auto-cleaning Pydantic model for scraped data:

```python
from cmdop import SDKBaseModel

class Product(SDKBaseModel):
    __base_url__ = "https://shop.com"
    name: str = ""    # "  iPhone 15  \n" → "iPhone 15"
    price: int = 0    # "$1,299.00" → 1299
    rating: float = 0 # "4.5 stars" → 4.5
    url: str = ""     # "/p/123" → "https://shop.com/p/123"

products = Product.from_list(raw["items"])  # Auto dedupe + filter
```

---

## Utilities

**Logging:**
```python
from cmdop import get_logger
log = get_logger(__name__)
log.info("Starting")  # Rich console + auto file logging
```

**TOON Format (30-50% token savings):**
```python
from cmdop import json_to_toon, JsonCleaner
toon = json_to_toon({"name": "Alice", "age": 25})
# → "name: Alice\nage: 25"
```

---

## Requirements

- Python 3.10+
- CMDOP agent on target

## Links

[cmdop.com](https://cmdop.com)

## License

MIT
