Catalog PyPI
PACKAGE EXPLORERView in registry

aimax

PyPI

Drive Claude Code and Codex TUIs through tmux from Python.

agentasyncioautomationclaudeclaude-codeclicodextmux
Versions collected1
Direct dependencies4From release 0.1.0
Last observed21 Oct, 11:15 UTC

README

Version 0.1.0 documentation

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 varDefaultNotes
AIMAX_DATA_DIR$XDG_DATA_HOME/aimaxWhere JSON state lives.
AIMAX_HOME~Used by the default expansions
AIMAX_CODEX_HOME$CODEX_HOME or ~/.codex
AIMAX_CLAUDE_HUBimac_claude_code_agent_hubtmux session name
AIMAX_CODEX_HUBimac_codex_agent_hubtmux 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.sqliteRead-only SQLite.
AIMAX_CODEX_SESSIONS_DIR~/.codex/sessions
AIMAX_CODEX_DEFAULT_MODELgpt-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_PREFLIGHT1Set 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)

MethodReturns
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):

KeyRequiredNotes
sessionIdyesBecomes the tmux window name.
cwdyes for createWorking dir; must exist.
initialPromptyes for createFirst message to send.
promptyes for send / queueMessage to send.
flagRootnoAnchor for running.flag. Defaults to cwd.
modelnoForwarded as --model to the agent.
useProxynoForce proxychains on/off; default = admin setting.
displayNamenoFree-form label persisted alongside the runtime entry.
agentSessionIdnoResume 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.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.1.011 Jun 20264

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Metadata history

Meaningful changes
Current collected state Current

Observed since 21 Oct, 11:15 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:f2d675f2d34674ac0ffd4f78e7f335564ac3f58c7a9d6cc014378e70625d246e