Getting Started
indusagiis the terminal-first AI coding-agent framework for Python. It ships both thepindusagiCLI and an embeddable library surface organized by capability layer. Install withpip install indusagi, thenimport indusagi(or a single subpackage such asimport indusagi.llmgateway).
Table of Contents
- Install
- The Agent library quickstart
- Gateway-direct quickstart
- The pindusagi CLI quickstart
- Auth and credentials
- Notes
Install
pip install indusagi
The core install pulls in only the wire-level dependencies (httpx, pydantic,
wcwidth, python-ulid, regex) — no vendor SDKs. Two optional extras add the
interactive surface and external-tool mounting:
# interactive Textual TUI + syntax highlighting + markdown rendering
pip install "indusagi[tui]"
# Model Context Protocol server mounting
pip install "indusagi[mcp]"
# both at once
pip install "indusagi[mcp,tui]"
indusagi requires Python 3.11+. Installing it also registers two equivalent
console scripts, indusagi and pindusagi, both wired to
indusagi.shell_app.cli:run.
Confirm the install and read the version the way the runtime does — it is resolved from package metadata, never hardcoded:
import indusagi
print(indusagi.__version__) # e.g. '0.1.2' when installed
print(indusagi.VERSION) # same value
# Subsystems are lazily re-exported as namespaces:
gw = indusagi.gateway # -> indusagi.llmgateway
rt = indusagi.runtime # -> indusagi.runtime
The Agent library quickstart
The high-level entry point is the Agent class from
indusagi.agent. You construct one, attach a tool collection bound to a working
directory, and drive it with await agent.prompt(...). The whole surface is
async/await.
import asyncio
from indusagi.agent import Agent, create_coding_tools
async def main():
agent = Agent()
agent.set_model("claude-sonnet-4") # a catalog id (see the gateway)
agent.set_system_prompt("You are a helpful coding assistant.")
agent.set_tools(create_coding_tools("/path/to/project"))
# Tap the live event stream; subscribe() returns an unsubscribe disposer.
unsubscribe = agent.subscribe(lambda ev: print(ev.type))
# prompt() drives the run to settlement; it returns None — results land
# on agent.state.messages.
await agent.prompt("explain this repository")
unsubscribe()
for msg in agent.state.messages:
print(getattr(msg, "role", None))
asyncio.run(main())
Agent() accepts an optional AgentOptions mapping (or kwargs) — steering_mode,
follow_up_mode, session_id, get_api_key, thinking_budgets, and a few more.
The model is set with set_model(...), tools with set_tools(...), and the system
preamble with set_system_prompt(...). The provider key (e.g. ANTHROPIC_API_KEY)
is read from the environment automatically.
create_coding_tools(cwd) builds the read/write/bash/edit/process/todo/web tool set;
create_read_only_tools(cwd) is the safe subset (read/grep/find/ls/todoread/web).
You can also steer() a live run, queue follow_up() prompts, abort(), and
await agent.wait_for_idle():
import asyncio
from indusagi.agent import Agent, create_read_only_tools
async def main():
agent = Agent(steering_mode="one-at-a-time", follow_up_mode="all")
agent.set_model("claude-sonnet-4")
agent.set_tools(create_read_only_tools("."))
agent.set_thinking_level("medium")
task = asyncio.create_task(agent.prompt("survey the codebase"))
agent.steer("focus on the agent package") # folds into the live run
agent.follow_up("now summarize what you found") # held until idle
await task
await agent.wait_for_idle()
asyncio.run(main())
See Agent facade for the full surface (sessions, custom message kinds, the event vocabulary) and Capabilities for the built-in tools.
Gateway-direct quickstart
If you only need raw model access — no agent loop, no tools — talk straight to the
LLM Gateway. It resolves a model id to a ModelCard,
routes to the connector that speaks the card's wire dialect, and returns either a
streaming channel of Emissions (stream) or an assembled Reply (complete).
import asyncio
from indusagi.llmgateway import (
complete, get_card, estimate_cost,
Conversation, UserTurn, TextBlock, StreamOptions,
)
async def main():
convo = Conversation(
turns=(UserTurn(blocks=(TextBlock(text="summarize the repo"),)),),
system="Be terse.",
)
opts = StreamOptions(temperature=0.2, max_output_tokens=512, thinking="low")
reply = await complete("claude-sonnet-4", convo, opts)
print(reply.stop, reply.usage)
card = get_card("claude-sonnet-4") # context window / pricing / wire dialect
if card:
print(estimate_cost(card, reply.usage), "USD")
asyncio.run(main())
stream() is synchronous and lazy — nothing hits the network until you iterate, and
the returned channel is re-iterable (re-iterating re-issues the request):
import asyncio
from indusagi.llmgateway import stream, Conversation, UserTurn, TextBlock
from indusagi.llmgateway import TextEmission, DoneEmission
async def main():
convo = Conversation(turns=(UserTurn(blocks=(TextBlock(text="hello"),)),))
channel = stream("claude-sonnet-4", convo) # no I/O yet
async for emission in channel: # I/O happens here
if isinstance(emission, TextEmission):
print(emission.delta, end="")
elif isinstance(emission, DoneEmission):
print("\nstop:", emission.reply.stop)
asyncio.run(main())
get_card(id) reaches beyond the curated cards (claude-opus-4, claude-sonnet-4,
gpt-4o, o3, gemini-2.5-pro, kimi-k2, mock-1, …) into the full generated
catalog as a fallback; unknown ids raise an unsupported GatewayError. The
models() fluent query (.by_provider(...), .by_api(...), .reasoning(...),
.all(), .find(id)) lists what is available.
If you prefer the old-vocabulary (camelCase) facade — Model, AssistantMessage,
stream/complete over message objects — use indusagi.ai
instead; it is a thin shim over the same gateway core.
The pindusagi CLI quickstart
pindusagi is the command-line front door. The runner is selected from your flags:
-p for one-shot print, --json for the wire protocol, and a bare invocation for
the interactive TUI. Set the provider key for the model you intend to run first.
export ANTHROPIC_API_KEY="sk-..."
# Version and usage (rendered from the flag table)
pindusagi --version # prints: indusagi <version>
pindusagi --help
# One-shot: print a single answer and exit
pindusagi -m claude-sonnet-4 -p "summarize the pyproject.toml in this repo"
# Wire mode: line-delimited JSON protocol over stdio (also --rpc / --wire)
pindusagi -m claude-sonnet-4 --json
# Interactive Textual TUI (the default when attended; needs the [tui] extra)
pindusagi -m claude-sonnet-4
The full flag vocabulary (the single source of truth is the FLAG_SPECS table):
| Flag | Aliases | Kind | Purpose |
|---|---|---|---|
--model |
-m |
string | Catalog model id (else settings.default_model, else claude-sonnet-4) |
--print |
-p |
boolean | One-shot print mode: stream the answer, then exit |
--json |
--rpc, --wire |
boolean | NDJSON wire protocol over stdio |
--interactive |
-i |
boolean | Force the interactive REPL/TUI |
--cwd |
— | string | Working directory for tools |
--system |
— | string | Override the system prompt |
--no-tools |
— | boolean | Disable all built-in tools |
--mcp |
— | string (repeatable) | Mount an MCP server (URL → SSE, else a stdio command) |
--help |
-h |
boolean | Render usage and exit |
--version |
-v |
boolean | Print the version and exit |
The launcher derives one RunnerMode (print / wire / repl / help / version)
via a fixed precedence ladder: help > version > json/wire > interactive > print >
(attended ? repl : print). So an unattended pipe defaults to print and an attended
terminal defaults to the interactive TUI.
Attach external Model Context Protocol servers by repeating --mcp — an http(s)
URL becomes an SSE server (id from hostname), anything else is split into a stdio
command (id from basename):
pindusagi -m claude-sonnet-4 --mcp ./my-server --mcp https://tools.example.com -p "what tools do you have?"
You can also drive the CLI in-process from Python — main(argv) returns an exit code
and never calls sys.exit:
import asyncio
from indusagi.shell_app import main
# Equivalent to: pindusagi --print "explain this repo"
exit_code = asyncio.run(main(["--print", "explain", "this", "repo"]))
print("exited with", exit_code)
See Shell App for the boot pipeline and runner internals, and CLI reference for the complete flag and mode documentation.
Auth and credentials
The auth subcommand bypasses flag parsing and manages stored credentials
(login / refresh / status):
pindusagi auth login anthropic # API-key first: prints an ANTHROPIC_API_KEY hint
pindusagi auth login openai # OAuth-capable provider (paste-based PKCE flow)
pindusagi auth status # show what is stored
pindusagi auth refresh openai # refresh an OAuth token
For Anthropic the simplest path is to set ANTHROPIC_API_KEY in the environment;
auth login anthropic just prints that hint and exits. Other providers read their
own env keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.) through the gateway's
SECRET_TABLE. Persisted credentials live under this build's own profile directory,
~/.pindusagi/auth.json (override the home root with INDUSAGI_HOME).
Notes
- Requires Python 3.11+; the package is
indusagion PyPI. - Two equivalent console scripts are installed:
pindusagiandindusagi(both →indusagi.shell_app.cli:run). - The interactive TUI needs the
[tui]extra; MCP mounting needs[mcp]. - State (settings, sessions, auth, logs) lives under
~/.pindusagi/by default; setINDUSAGI_HOMEto relocate it. - No vendor SDKs are required — every provider is reached over raw HTTP via
httpx. - For the full namespace and subpath map, see Package Exports.
