Metadata-Version: 2.4
Name: node-mantis
Version: 0.1.1
Summary: NodeMantis Python SDK: browser-driving agent sessions plus setup and installation validation tools
Author: NodeMantis
License-Expression: LicenseRef-NodeMantis-Proprietary
Project-URL: Homepage, https://github.com/Node-Mantis/CleanSlate#readme
Project-URL: Repository, https://github.com/Node-Mantis/CleanSlate
Project-URL: Issues, https://github.com/Node-Mantis/CleanSlate/issues
Keywords: browser,automation,node-mantis,nodemantis
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Dynamic: license-file

# NodeMantis Python

The NodeMantis Python SDK: browser-driving agent sessions at parity with the
npm `node-mantis` package, plus configuration and installation validation
tools.

Platform wheels bundle the same compiled native runtime the npm SDK uses; the
Python layer is a thin client that spawns it and forwards requests. The
runtime launches and drives your installed Google Chrome itself; the package
has no Playwright dependency.

Requirements: Google Chrome installed in its standard location, or `CHROME_PATH`
set to a Chrome executable. `python -m nodemantis doctor` reports whether
NodeMantis can launch it and, if not, how to install it.

Customer install:

```bash
pip install node-mantis
python -m nodemantis configure --api-key "$NODEMANTIS_API_KEY"
python -m nodemantis doctor
```

`doctor` checks the installation end to end and says how to fix what fails:
which copy of the CLI runs (and any other `nodemantis` on `PATH`, for example
from Homebrew or npm), whether a newer release is on PyPI, credentials, the
agent engine, Google Chrome on a local test page, and the hosted API.
`--offline` skips the network checks. `python -m nodemantis --version` (or
`-v`) prints the version.

Run a browser goal:

```python
import asyncio
from nodemantis import NodeMantis


async def main():
    async with await NodeMantis.start(start_url="https://example.com", headless=True) as mantis:
        result = await mantis.run("Open the More information link.", max_iterations=5)
        print(result.ok, result.reason)

asyncio.run(main())
```

Managed sessions share compatible browser processes by default, with a separate
browser context per session. Set `shared_browser=False` for a private browser.
Closing one session leaves other shared sessions running.

The session surface mirrors the TypeScript SDK: `NodeMantis.start()` /
`NodeMantis.preflight()`, and per-session `go`, `run`, `retrieve`,
`extract`, `check`, `close` (sessions are context managers). Options are
snake_case (`max_iterations`, `result_schema`,
…); `effort` selects a capability level (`"low"`, `"medium"`,
`"high"`, `"xhigh"`). `service_tier` is `"flex"` (default) or `"standard"`:
flex uses the model providers' discounted tier, usually about half the cost
of standard, but can respond slowly while the provider's flex capacity is
busy; models without a flex tier always run at standard. The CLI takes
`--tier flex|standard` (or `NODEMANTIS_TIER`) and prints one warning, at most
hourly, when the model is slow on flex. `run` accepts an `on_progress` callback for per-iteration
narration; callbacks arrive on a background thread and must not call session
methods. `retrieve` returns a discriminated result with `status` of `success`
or `unavailable`. Success carries non-null `data`; unavailable carries
`data=None`, a client-readable `message`, and a `retryable` boolean. Both
outcomes include token usage, metered cost, the final URL, and an optional
trace ID. Schemas are plain JSON Schema dicts (pydantic v2 model classes are
also accepted).

`mantis.page` is a bounded asynchronous `ManagedPage`, not a Playwright page.
It supports navigation/history, URL and title reads, binary screenshots,
cookies, local/session storage state, viewport control, file choosers, and
downloads. It intentionally has no evaluation, selector/locator, event, or
arbitrary-CDP escape hatch.

```python
import asyncio
from nodemantis import NodeMantis


async def main():
    async with await NodeMantis.start(headless=True) as mantis:
        await mantis.page.navigate("https://example.com", wait_until="load")
        print(await mantis.page.title())
        png = await mantis.page.screenshot(full_page=True)

asyncio.run(main())
```

If NodeMantis cannot complete a retrieval request, it raises
`RetrievalRequestError` rather than returning `unavailable`. The exception
contains only customer-actionable fields: `code`, `retryable`, `trace_id`, and
`cost_usd`, along with its message. Codes are `INVALID_REQUEST`,
`AUTHENTICATION_FAILED`, `USAGE_LIMIT`, `REQUEST_BLOCKED`,
`SERVICE_UNAVAILABLE`, `CANCELLED`, or `OPERATION_FAILED`.

```python
import asyncio
from nodemantis import NodeMantis, RetrievalRequestError


async def main():
    async with await NodeMantis.start(start_url="https://sandbox.nodemantis.com") as mantis:
        try:
            result = await mantis.retrieve("Retrieve the featured books and their prices.")
            if result.is_unavailable and result.retryable:
                print("No result was available; trying again may help.")
        except RetrievalRequestError as error:
            print(error.code, error.retryable)
            raise


asyncio.run(main())
```

Managed sessions accept the raw-CDP launcher's serializable context settings
as a plain `context_options` dictionary, using the same camel-case option names
as TypeScript `contextOptions`. NodeMantis owns the user agent and
device-class identity, so
`userAgent`, `isMobile`, and `hasTouch` are reserved and must not be supplied
there.

```python
import asyncio
from nodemantis import NodeMantis


async def main():
    async with await NodeMantis.start(
        headless=True,
        viewport={"width": 1280, "height": 720},
        context_options={
            "locale": "en-CA",
            "timezoneId": "America/Toronto",
            "screen": {"width": 1440, "height": 900},
            "deviceScaleFactor": 2,
        },
    ) as localized:
        await localized.go("https://example.com")

asyncio.run(main())
```

### How Phase 1 chooses a browser identity

NodeMantis chooses one coherent identity when a managed session starts and
keeps it for the life of that session. Supported values supplied by the caller
come first, followed by facts read from the installed Chrome and the host. If
those facts cannot be read, NodeMantis uses a platform-specific fallback whose
screen, viewport, language, and device scale still agree. For any value Chrome
cannot represent honestly, NodeMantis keeps Chrome's native value instead of
inventing one or replacing a JavaScript getter.

If no viewport is supplied, the managed-session default remains 1280×720.
Screen size and DPR are detected separately or taken from the coherent
platform fallback; they are not inferred by pretending the viewport is the
whole display.

The resulting profile is applied through browser controls, without page-level
JavaScript identity wrappers. Managed sessions work in both headless and headed
mode, and each session keeps its chosen profile fixed across pages, frames,
popups, and dedicated, shared, and service workers. HTTP requests retain the
resolved full `Accept-Language` preference, while browser JavaScript
consistently sees the selected primary locale. If an unrecognized Chrome
version cannot provide a coherent live Client Hints fingerprint, NodeMantis
leaves those values native rather than fabricating a brand list.

Before a managed page or worker starts running site code, NodeMantis asks
Chrome to hold it briefly, applies the native parts of the session profile,
and then lets it continue. The hold applies only to the browser context owned
by that NodeMantis session, so unrelated Playwright browsers and contexts keep
working normally. Popups and workers created later go through the same check,
including workers created by another worker.

Cross-origin frames use a safe exception to that hold because pausing both the
frame and its parent can block Chrome from attaching either one. They receive
the same launch, context, network, and top-page settings before their first
instruction, then NodeMantis reinforces the values Chrome allows on the frame
itself. If Chrome cannot apply a value there, the frame keeps its honest native
value; NodeMantis never installs a JavaScript getter to disguise the mismatch.

Pages and workers can disappear while Chrome is still setting them up. When
that happens, NodeMantis asks Chrome whether the target really ended. If it did,
the rest of the session continues; if it is still alive with only part of the
profile applied, NodeMantis closes it. If Chrome cannot give a definite answer,
that browser instance is retired and a later session starts a fresh one. This
keeps headless startup from returning a half-configured page or reusing a
browser whose shutdown was uncertain.

Chrome version, User-Agent, and Client Hints must describe the browser that is
actually running. Consequently, `userAgent`, `isMobile`, and `hasTouch` remain
reserved context options. `User-Agent` and every `Sec-CH-UA*` name are also
reserved inside `context_options["extraHTTPHeaders"]`; attempts to override
them are rejected instead of creating a mismatch between JavaScript and
network requests.

Phase 1 makes Chrome's own identity internally consistent; it does not claim
that headless Chrome defeats every detector. Graphics, the observable effects
of automation and CDP, networking, and the agent's functional page scripts are
separate hardening areas.

API keys resolve from the `api_key` argument, then `NODEMANTIS_API_KEY`,
`NODEMANTIS_AUTH_TOKEN`, and finally the `nodemantis configure` config file.
The package always connects to the hosted Node Mantis API; the service
endpoint is not customer-configurable.

NodeMantis does not install a browser. It drives the Google Chrome already on
the machine (or the one `CHROME_PATH` names); `doctor` checks it by
opening a real NodeMantis session and print per-platform install steps when
Chrome is missing.

Installing the wheel also puts a `nodemantis` command on `PATH` (equivalent to
`python -m nodemantis`); `pipx install node-mantis` gives the same command in
an isolated environment. It provides `run`, `retrieve`, `configure`, and
`doctor`, like the npm package's CLI. If both are installed, whichever is
first on `PATH` wins; `nodemantis doctor` shows which one that is.

Validate API access, credentials, and Google Chrome from Python:

```python
import asyncio
from nodemantis import NodeMantis


async def main():
    report = await NodeMantis.preflight(check_browser=True, timeout_ms=5000)
    if not report.ok:
        raise RuntimeError(report.checks)

asyncio.run(main())
```

Supported platforms: Linux x64, Windows x64, macOS x64, and macOS arm64.
Wheels without a bundled runtime (source-free builds) raise
`RUNTIME_NOT_INSTALLED` when starting a session; advanced setups can point
`NODEMANTIS_RUNTIME_PATH` at a runtime executable.

This package is proprietary software. Use is governed by `LICENSE.txt`
and applicable NodeMantis terms.

### Keep a session open between calls

Use `run()` for a complete goal or a smaller instruction. Each call returns
control to Python while the browser session stays open. Run your own processing,
query a database, or call another service before continuing. The browser session stays open until you close it or terminate the program.

```python
import asyncio
from nodemantis import NodeMantis


async def main():
    async with await NodeMantis.start(start_url="https://sandbox.nodemantis.com") as mantis:
        await mantis.run("Open the Travel & Adventure category.")
        order = {"title": "Journey to the Center of the Earth", "quantity": 1}
        await mantis.run(f'Open "{order["title"]}" and add {order["quantity"]} copy to the cart.')

asyncio.run(main())
```

### Migrating from `step()`

`step()` is deprecated and emits `DeprecationWarning`. Existing calls retain
their bounded-step behavior for compatibility. Use `run()` for new code.
Replace `max_actions` with `max_iterations` when preserving a limit, and remove
`allow_navigation`: `run()` permits navigation. The default iteration limit
changes from 15 to 30, and the special bounded-step prompt no longer applies.
Session defaults using `max_actions` only affect legacy `step()` calls; use
`max_iterations` for `run()`.

## Async migration

Session and page operations now require `await`, including `NodeMantis.start()`,
`NodeMantis.preflight()`, and `close()`. Use `async with await NodeMantis.start()`
for automatic cleanup, and `asyncio.run(main())` at your program entry point.
Inside an existing event loop, await your coroutine directly.

Cancel an operation with `asyncio.Task.cancel()` or bound it with
`asyncio.wait_for()`. Session mutations are serialized; download and file-chooser
waits can run concurrently with the action that triggers them.

Installation helpers are no longer package-root SDK exports. Use
`nodemantis configure` and `nodemantis doctor` for setup and diagnostics. Use `await NodeMantis.preflight()` for application preflight.
Both package CLIs also support `nodemantis run` and `nodemantis retrieve`.
