Skip to main content

Drive Claude Code and Codex TUIs through tmux from Python.

Project description

aimax

PyPI Python 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

pip install aimax

Or from source:

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)

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)

# 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

# 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. 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:

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)

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:

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.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aimax-0.1.0.tar.gz (75.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aimax-0.1.0-py3-none-any.whl (69.1 kB view details)

Uploaded Python 3

File details

Details for the file aimax-0.1.0.tar.gz.

File metadata

  • Download URL: aimax-0.1.0.tar.gz
  • Upload date:
  • Size: 75.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for aimax-0.1.0.tar.gz
Algorithm Hash digest
SHA256 38d61318c9b4e02e05dcd3159e7023b3968e764673004a857f1c851f31b7c1bd
MD5 d20a40d0ebc7a9b7add0a1158e23546d
BLAKE2b-256 5974237b0d0f28fe94585f8a10e1df625c9b7d1ddd9b1d9c96f9a5541e95684a

See more details on using hashes here.

File details

Details for the file aimax-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: aimax-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 69.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for aimax-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2b46153c30d5a3a63a7be8706ea416bb8797d390e86428e7f426e13320ae5754
MD5 444a1d0b0f72a20d965ca834d0e4e5da
BLAKE2b-256 2471ab14d4d7714a92d0ebe7ca27c33a4e57cd2d861391be44f330f63cf1fbe5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page