Metadata-Version: 2.4
Name: aimax
Version: 0.1.0
Summary: Drive Claude Code and Codex TUIs through tmux from Python.
Project-URL: Homepage, https://pypi.org/project/aimax/
Project-URL: Documentation, https://pypi.org/project/aimax/
Project-URL: Source, https://pypi.org/project/aimax/
Project-URL: Changelog, https://pypi.org/project/aimax/
Author: aimax contributors
License: MIT License
        
        Copyright (c) 2026 aimax contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agent,asyncio,automation,claude,claude-code,cli,codex,tmux
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: Terminals
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# aimax

[![PyPI](https://img.shields.io/pypi/v/aimax.svg)](https://pypi.org/project/aimax/)
[![Python](https://img.shields.io/pypi/pyversions/aimax.svg)](https://pypi.org/project/aimax/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Drive **Anthropic's Claude Code TUI** and **OpenAI's Codex TUI** from
Python through `tmux`. One unified `asyncio` API + one friendly CLI for
both agents.

> No SDK reverse-engineering. No headless mode. No browser automation.
> Just a real interactive TUI in a real tmux window, with a JSONL
> file-tail giving you the agent's live thought stream.

---

## Why?

Both Claude Code and Codex ship great interactive TUIs but have no
stable headless API. People who want to integrate them into larger
systems typically end up:

* writing brittle screen-scrapers, or
* shelling out to one-shot `--bare` invocations and losing the agent's
  long-running conversation state.

This package takes a different approach borrowed from the *imac*
project: **run the real TUI in a tmux window** and use tmux's
load-buffer + paste-buffer primitives to feed it input. The agent
persists its conversation to a JSONL file anyway, so we tail that file
for the live output stream.

The result is a clean, async-friendly Python API that survives backend
restarts (the tmux windows keep running) and integrates cleanly with
WebSocket-style streaming consumers.

## Features

* **Two backends, one API**: `TmuxClaudeCodeBackend` and
  `TmuxCodexBackend` both implement the same `AgentBackend` contract.
* **Real conversations**: sessions stay alive across CLI invocations
  and even across backend-process restarts.
* **JSONL thought-stream**: subscribe to a per-session event bus, or
  pull a history snapshot and resume tailing from an exact byte offset
  with no duplicates and no gaps.
* **Job-completion signal**: the agent drops a `running.flag` file
  when its turn is done; `is_job_goal_accomplished()` and
  `is_failed()` reflect that in milliseconds.
* **Configurable**: every hardcoded path from the original JS lives in
  one dataclass; environment variables override at runtime.
* **Friendly CLI**: `aimax create / send / pause / stop / list /
  status / history / stream` — all with `--json` output for scripts.
* **Zero runtime dependencies**: stdlib only. (Python ≥ 3.9.)

## Requirements

* Python ≥ 3.9.
* `tmux` on `$PATH`.
* `claude` (for Claude Code) and/or `codex` (for Codex) on `$PATH`.
* Optional, for the proxychains path: `proxychains` plus a
  `~/proxy_envs.bash` and `~/proxy_claude.conf` (all configurable).

The package never installs the agent binaries — they each ship their
own installers (`npm`, the Codex installer, etc.).

## Installation

```bash
pip install aimax
```

Or from source:

```bash
cd mobius/backend/agents_py
pip install -e .
```

The wheel weighs a few KB — there are no compiled extensions and no
third-party Python deps.

## Quick start (Python)

```python
import asyncio
import aimax

async def main():
    backend = aimax.get("tmux-codex")

    # Spawn a fresh session and submit the first prompt.
    handle = await backend.create_new_session({
        "sessionId":     "demo-1",
        "cwd":           "/tmp/sandbox",
        "initialPrompt": "Make a hello.py that prints hi.",
    })
    print("Started, agent thread =", handle["agentSessionId"])

    # Subscribe to live events.
    def on_event(raw):
        print("event:", raw.get("type"), raw.get("payload", {}).get("type"))

    unsubscribe = backend.get_agent_raw_thought_stream("demo-1", on_event)

    # Send a follow-up without interrupting.
    await backend.no_pause_current_and_queue_query_at_session({
        "sessionId": "demo-1",
        "prompt":    "Also add a README.",
    })

    # ... eventually ...
    unsubscribe()
    await backend.terminate_session("demo-1")

asyncio.run(main())
```

## Quick start (CLI)

```bash
# Start a session
aimax create -b tmux-codex -s demo --cwd /tmp/sandbox \
    -p 'Make a hello.py that prints hi.'

# Add a follow-up — won't interrupt the current turn
aimax send -b tmux-codex -s demo \
    -p 'Also add a README.'

# Watch the live event stream (Ctrl-C to stop)
aimax stream -b tmux-codex -s demo

# Quick health check
aimax status -b tmux-codex -s demo

# Inspect everything that's running
aimax list

# Clean up
aimax stop -b tmux-codex -s demo
```

Every command accepts `--json` for machine-readable output, and reads
the prompt from `--prompt` / `--prompt-file FILE` / stdin (pipe).

### History + live stitched together

```bash
# Dump history as JSON; the `sentinel` is a byte offset.
aimax history -b tmux-codex -s demo --json > history.json
SENTINEL=$(jq -r .sentinel history.json)

# Resume the live tail from exactly that offset — no duplicates.
aimax stream -b tmux-codex -s demo --from-sentinel "$SENTINEL"
```

## Configuration

Every filesystem path lives in
[`aimax.config.TmuxAgentsConfig`](src/aimax/config.py).
The defaults follow XDG conventions; you can override any of them via
environment variable:

| Env var                              | Default                                     | Notes                          |
| ------------------------------------ | ------------------------------------------- | ------------------------------ |
| `AIMAX_DATA_DIR`               | `$XDG_DATA_HOME/aimax`                | Where JSON state lives.        |
| `AIMAX_HOME`                   | `~`                                         | Used by the default expansions |
| `AIMAX_CODEX_HOME`             | `$CODEX_HOME` or `~/.codex`                 |                                |
| `AIMAX_CLAUDE_HUB`             | `imac_claude_code_agent_hub`                | tmux session name              |
| `AIMAX_CODEX_HUB`              | `imac_codex_agent_hub`                      | tmux session name              |
| `AIMAX_CLAUDE_CONFIG`          | `~/.claude.json`                            |                                |
| `AIMAX_CLAUDE_SETTINGS`        | `~/.claude/settings.api.json`               |                                |
| `AIMAX_CLAUDE_PROJECTS_DIR`    | `~/.claude/projects`                        |                                |
| `AIMAX_CODEX_CONFIG`           | `~/.codex/config.toml`                      |                                |
| `AIMAX_CODEX_STATE_DB`         | `~/.codex/state_5.sqlite`                   | Read-only SQLite.              |
| `AIMAX_CODEX_SESSIONS_DIR`     | `~/.codex/sessions`                         |                                |
| `AIMAX_CODEX_DEFAULT_MODEL`    | `gpt-5.5`                                   |                                |
| `AIMAX_RIGHTCODE_ENV_FILE`     | `~/.codex/secrets/rightcode.env`            |                                |
| `AIMAX_PROXY_ENVS_BASH`        | `~/proxy_envs.bash`                         |                                |
| `AIMAX_PROXY_CHAINS_CONF`      | `~/proxy_claude.conf`                       |                                |
| `AIMAX_RUN_PREFLIGHT`          | `1`                                         | Set to `0` to skip startup checks. |

`aimax config show` prints every active value plus the full
env-var map.

For programmatic configuration:

```python
from pathlib import Path
import aimax
from aimax.config import TmuxAgentsConfig, set_config

set_config(TmuxAgentsConfig(
    data_dir=Path("/var/lib/myapp/aimax"),
    run_preflight=False,
))

backend = aimax.get("tmux-claude-code")
```

> Always call `set_config(...)` **before** the first `get(...)` — the
> backend snapshots its paths at construction time.

## API reference

### `aimax.get(name) -> AgentBackend`

Factory. Returns a singleton instance of the named backend. Valid
names are listed in `aimax.SUPPORTED_BACKENDS`
(`"tmux-claude-code"`, `"tmux-codex"`).

### `AgentBackend` (abstract surface — implemented by both backends)

| Method                                                              | Returns                                  |
| ------------------------------------------------------------------- | ---------------------------------------- |
| `async create_new_session(opts)`                                    | `{sessionId, agentSessionId, jsonlPath, startedAt}` |
| `async no_pause_current_and_queue_query_at_session(opts)`           | `None`                                   |
| `async pause_current_and_resume_from_session(opts)`                 | `None`                                   |
| `async terminate_session(session_id)`                               | `{sessionId, killed, wasWorking}`        |
| `is_alive(session_id)`                                              | `bool`                                   |
| `is_working(session_id)`                                            | `bool`                                   |
| `is_job_goal_accomplished(session_id)`                              | `bool`                                   |
| `is_failed(session_id)`                                             | `bool`                                   |
| `list_sessions()`                                                   | `list[dict]`                             |
| `get_history(session_id)`                                           | `{entries, total, truncated, sentinel}`  |
| `get_agent_raw_thought_stream(session_id, listener, opts=None)`     | unsubscribe callable                     |

`opts` for `create` / `send` is a dict with camelCase keys (the
contract matches the original JS API):

| Key                | Required | Notes                                                             |
| ------------------ | -------- | ----------------------------------------------------------------- |
| `sessionId`        | yes      | Becomes the tmux window name.                                     |
| `cwd`              | yes for `create` | Working dir; must exist.                                 |
| `initialPrompt`    | yes for `create` | First message to send.                                  |
| `prompt`           | yes for `send` / `queue` | Message to send.                                |
| `flagRoot`         | no       | Anchor for `running.flag`. Defaults to `cwd`.                      |
| `model`            | no       | Forwarded as `--model` to the agent.                              |
| `useProxy`         | no       | Force proxychains on/off; default = admin setting.                 |
| `displayName`      | no       | Free-form label persisted alongside the runtime entry.            |
| `agentSessionId`   | no       | Resume an existing agent thread id (uuid for Claude, thread id for Codex). |

### Prompt-paste recorder (analytics hook)

```python
from aimax.services import agent_prompt_events

def my_recorder(event):
    # event = {"backend_name": "...", "session_id": "...", "content_length": int}
    my_metrics.add(event)

agent_prompt_events.set_recorder(my_recorder)
```

After `set_recorder(...)`, every paste through any backend will hand
the event dict to your function.

## How it works (the 1-minute mental model)

```
         ┌─────────────────────────────────────────────┐
         │ tmux server (hub session per backend)       │
         │                                             │
         │   window: my-session                        │
         │     ┌─────────────────────────────────────┐ │
         │     │ bash -lc 'exec claude ...'          │ │
         │     │   ┌─────────────────────────────┐   │ │
         │     │   │ Claude TUI                  │   │ │
         │     │   │  ──────────                 │   │ │
         │     │   │  > user prompt              │◀──┼─┼──── tmux load-buffer
         │     │   │   assistant response...     │   │ │     + paste-buffer -p
         │     │   │                             │   │ │     + send-keys Enter×3
         │     │   └─────────────────────────────┘   │ │
         │     └──────────────────────────┬──────────┘ │
         │                                │            │
         └────────────────────────────────┼────────────┘
                                          ▼
                ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl
                                          │
                                          ▼
                                jsonl_watcher.watch(...)
                                          │
                                          ▼
                                 your `on_entry` callback
```

* **Input** goes through bracketed paste (`paste-buffer -p`) so embedded
  `\n` cannot prematurely submit; submission is via `send-keys Enter`
  three times (the TUI sometimes swallows the first while switching
  modes).
* **Output** is tailed from the JSONL the agent writes anyway. We
  share one watcher per session for the event bus, plus we can open
  private watchers from arbitrary byte offsets for history-resumption.
* **State** is persisted to two JSON files (`hub-runtime.json` and
  `hub-archive.json`) so backend restarts don't kill running agents.

## Job-completion convention

Every prompt submission writes
`<flag_root>/.imac/flags/<session_id>/running.flag`. By contract the
agent removes that file when its job is done (success *or* failure),
and writes `failed.flag` if it failed. Then:

```python
backend.is_job_goal_accomplished("my-session")  # ⇒ True once running.flag is gone
backend.is_failed("my-session")                 # ⇒ True iff failed.flag is on disk
```

This works because the flags are real files, so any process can read
them — your monitoring scripts don't need to talk to the backend
process at all.

## License

MIT. See [LICENSE](LICENSE).

## Acknowledgements

This package is a Python port of the JavaScript `agents/` module from
the *imac* project. The trade-offs and gotchas captured in the
docstrings (bracketed paste, Enter swallowing, screen-scrape trust
fallback, …) were learned the hard way by the original authors.
