Hire Team

INDUSAGI DOCUMENTATION

CLI Reference

Preserved documentation snapshot. APIs and capabilities differ by edition and version. Check the selected package before implementation. View pinned source.

View Python Framework source on GitHub
On this page

The pindusagi / indusagi command-line front door. It parses argv, assembles an immutable boot context through an ordered stage pipeline, and dispatches to one of three runners (one-shot print, NDJSON wire, or interactive REPL). The machinery lives in indusagi.shell_app; the console scripts map to indusagi.shell_app.cli:run.

Table of Contents

Invocation

Install the package and both console scripts (indusagi and pindusagi, identical entry points) become available:

pip install indusagi                 # core only
pip install "indusagi[mcp,tui]"      # add MCP server mounting + the Textual TUI
pindusagi [options] [prompt...]

Everything after the option flags is collected into a free-text prompt. A bare -- terminator forces every remaining token to be treated as a positional, even if it looks like a flag. The parser (tokenize_invocation) is pure and table-driven — there are no per-flag branches — and supports long (--name), short (-x), glued (--name=value, -xvalue), clustered (-ip = -i -p), and repeatable forms. A malformed invocation (unknown flag, missing value, unparseable number) is reported to stderr and exits 2.

Flags

Every flag is one declarative row in FLAG_SPECS; the --help text is generated from the same table. Requires Python 3.11+.

Flag Aliases Value Purpose
--model -m string Choose the model to run, by catalog id or alias.
--print -p boolean Emit a single answer to stdout and exit (one-shot mode).
--json --rpc, --wire boolean Speak the JSON line protocol over stdio (wire mode).
--interactive -i boolean Force the read-eval-print loop even with a prompt present.
--cwd — string Run as if started from this working directory.
--system — string Override the system prompt with the given text.
--no-tools — boolean Disable every tool; the model may only produce text.
--mcp — string (repeatable) Attach an MCP server (see grammar below).
--help -h boolean Show usage and exit.
--version -v boolean Print the version and exit.
export ANTHROPIC_API_KEY="sk-..."

# One-shot: print a single answer and exit
pindusagi -m claude-sonnet-4 -p "summarize the package.json in this repo"

# Wire mode: speak the JSON line protocol over stdio (also --rpc / --wire)
pindusagi -m claude-sonnet-4 --json

# Interactive REPL (the default when attended with no prompt)
pindusagi -m claude-sonnet-4

# Run from elsewhere, override the system prompt, no tools
pindusagi --cwd /repo --system "Be terse." --no-tools -p "what does this do?"

--mcp value grammar

--mcp is repeatable; each occurrence accumulates and attaches one external Model Context Protocol server. The value's shape decides the transport:

  • An http(s):// URL connects over SSE; the server id is derived from the URL hostname.
  • Anything else is shlex.split into a stdio command (the id is the command's basename).

Server ids are de-duplicated with a numeric suffix against any servers already declared in settings. A faulted server is warned about but never blocks startup. MCP mounting requires the mcp extra.

pindusagi --mcp "python -m my_server --port 0" --mcp https://tools.example.com/sse -p "list tools"

Modes

The launcher derives a single RunnerMode from the parsed flags, the presence of a prompt, and whether the session is attended (both stdin and stdout are TTYs). Precedence, highest first:

Mode Triggered by Runner Behaviour
help --help — Render usage from the flag table, exit 0.
version --version — Print indusagi <version>, exit 0.
wire --json / --rpc / --wire WireRunner Line-delimited JSON request/event/result protocol over stdio.
repl --interactive, or attended with no prompt ReplRunner Interactive loop; mounts the Textual UI when both streams are TTYs.
print a prompt is present, or unattended with no prompt OneShotRunner Stream the answer to stdout, then exit.

The help and version modes short-circuit without ever standing up an agent. For running modes, select_runner scans RUNNERS (OneShotRunner, WireRunner, ReplRunner) and returns the first whose accepts() matches the mode.

from indusagi.shell_app import tokenize_invocation

# Mode derivation in action
assert tokenize_invocation(["--json"]).mode == "wire"
assert tokenize_invocation([], attended=True).mode == "repl"
assert tokenize_invocation([], attended=False).mode == "print"
assert tokenize_invocation(["-p", "hello"]).mode == "print"
  • print (OneShotRunner) streams assistant text deltas to stdout, then exits; a faulted run exits 1.
  • wire (WireRunner) reads one JSON request per line off a worker thread and emits event/result/error lines, serialized byte-for-byte to a fixed JSON shape (camelCase field names, optional fields omitted not nulled).
  • repl (ReplRunner) mounts the Textual interactive surface from ui_bridge when both streams are TTYs (requires the tui extra); without a TTY it falls back to a plain-text line loop behind the InteractiveView seam.

Auth Subcommand

pindusagi auth ... bypasses flag parsing entirely (its words would otherwise be read as a prompt) and dispatches to run_auth_command. It manages OAuth sign-ins for providers that support them; the resulting tokens land in the profile credential store and are never printed.

Subcommand Args Purpose
auth login <provider> provider id Start the PKCE authorization-code flow: print the auth URL, accept the pasted code, exchange it for tokens, persist them.
auth refresh <provider> provider id Use a stored refresh token to mint a fresh access token, then write the rotated credentials back.
auth status [provider] optional provider id Report which providers hold stored credentials (and their freshness), plus which provider API-key env vars are set (names only — never values).
pindusagi auth status            # what's stored + which API-key env vars are set
pindusagi auth login openai      # browser PKCE flow, paste the code back
pindusagi auth refresh openai    # rotate the access token

This surface is API-key-first for Anthropic: auth login anthropic and auth refresh anthropic just print a pointer at ANTHROPIC_API_KEY and exit 0 — only OAuth-capable providers (currently openai) are advertised. The flow is paste-based: there is no local callback server, so you paste either the bare authorization code or the entire redirect URL (the code= parameter is extracted from the latter). The credential store at ~/.pindusagi/auth.json is byte-compatible with the TypeScript builds' format but lives under this build's own profile directory and is never shared.

Environment Variables

The CLI reads provider API keys through the gateway's SECRET_TABLE (the first present, non-empty variable per provider wins) — see LLM Gateway for the full table. The common ones:

Variable Provider
ANTHROPIC_API_KEY Anthropic (the default claude-sonnet-4 fallback)
OPENAI_API_KEY OpenAI
GEMINI_API_KEY / GOOGLE_API_KEY Google
AZURE_OPENAI_API_KEY / AZURE_API_KEY Azure
NVIDIA_API_KEY NVIDIA
MOONSHOT_API_KEY / KIMI_API_KEY Kimi
Variable Effect
INDUSAGI_HOME When set and non-empty, names the profile/state directory directly, superseding the default ~/.pindusagi.

The framework reads every branded variable through a single env registry — subsystems never probe os.environ directly. Branded names are composed as INDUSAGI_<SUFFIX> (so env_name("home") yields INDUSAGI_HOME).

State Directory

All per-user state lives under the profile directory, resolved by the Locator. The default is ~/.pindusagi (override with INDUSAGI_HOME):

Path method Default location Holds
profile_dir() ~/.pindusagi/ Root for all per-user state.
settings_path() ~/.pindusagi/settings.json Global settings.
auth_store_path() ~/.pindusagi/auth.json OAuth credential store.
sessions_dir() ~/.pindusagi/sessions/ Persisted conversation sessions.
logs_dir() ~/.pindusagi/logs/ Diagnostic and crash logs.
upgrade_marker_path() ~/.pindusagi/upgrades.json Idempotent-upgrade marker.
project_settings_path(cwd) <project>/.pindusagi/settings.json Per-project overrides.
from indusagi.shell_app import Locator, BRAND

loc = Locator()
print(BRAND.bin_name)          # 'indusagi'
print(loc.settings_path())     # ~/.pindusagi/settings.json
print(loc.auth_store_path())   # ~/.pindusagi/auth.json

Settings

Settings merge from three layers, lowest precedence first: built-in DEFAULT_SETTINGS (collection coding), the global profile file, then the per-project file (later wins). A missing, unreadable, or malformed file contributes nothing rather than raising. On-disk JSON keys stay camelCase:

JSON key Field Meaning
defaultModel default_model Model id when no -m override is given.
systemPrompt system_prompt System preamble prepended to every run.
tools.collection tools.collection Built-in tool collection: read-only, coding, or all.
mcpServers mcp_servers External MCP servers connected at start-up.
compaction.triggerRatio compaction.trigger_ratio Context-window fraction (0..1) at which history condensation triggers.
compaction.keepRecent compaction.keep_recent Trailing turns kept untouched during condensation.

Model selection (resolve_model_id) layers the -m override over settings.default_model over the hard-coded claude-sonnet-4 fallback; each candidate is validated against the catalog via get_card, so an unknown id is skipped rather than honored.

import asyncio
from indusagi.shell_app import Locator, load_settings, resolve_model_id

loc = Locator()
settings = asyncio.run(load_settings(loc, "."))
print(resolve_model_id(settings, invocation_model="claude-sonnet-4"))

Exit Codes

Code Meaning
0 Success (including --help / --version and API-key-first auth login).
1 Startup failure or a faulted run.
2 Bad usage — unparseable argv or an unknown auth subcommand.
130 A KeyboardInterrupt (SIGINT) escaped the loop.

main() never calls sys.exit — it returns the code. Only the run() console-script shim maps it onto the process and translates KeyboardInterrupt to 130. A finally always runs the boot context's closables in reverse registration order (suppressing Exception, never CancelledError) so MCP fleets and other resources close cleanly.

Public API

indusagi.shell_app re-exports the entry points; indusagi.shell is an alias of the same package.

Name Kind Source Purpose
main async function shell_app/cli.py Drive one CLI invocation to an exit code; never calls sys.exit.
run function shell_app/cli.py The sync console-script shim wired to indusagi/pindusagi; asyncio.run(main()) then maps the code onto sys.exit.
build_boot_context async function shell_app/cli.py Assemble a runner-ready CliBootContext from a parsed invocation plus streams.
render_usage function shell_app/cli.py Build the full --help text from FLAG_SPECS and the brand name.
resolve_version function shell_app/cli.py Resolve the package version from indusagi.__version__; never raises.
CliStreams dataclass shell_app/cli.py The frozen input/output/error stream triple fed to runners.
CliBootContext dataclass shell_app/cli.py BootContext subclass adding the mutable closables teardown list.
tokenize_invocation function shell_app/invocation/parse.py Pure, table-driven parser: argv slice (+ attended) → Invocation; raises InvocationError.
Invocation dataclass shell_app/invocation/parse.py Parse result: flags dict, positionals, derived prompt, and the mode.
InvocationError class shell_app/invocation/parse.py Raised for unparseable argv; caught at the edge → exit 2.
RunnerMode type shell_app/invocation/parse.py Literal["print", "wire", "repl", "help", "version"].
FLAG_SPECS const shell_app/invocation/flags.py The canonical tuple of flag rows — the single source of CLI vocabulary.
FlagSpec dataclass shell_app/invocation/flags.py One flag's declarative description: name, kind, description, aliases, default, repeatable.
select_runner function shell_app/runners/registry.py Pick the first Runner whose accepts() is true; raises NoRunnerError.
RUNNERS const shell_app/runners/registry.py Ordered tuple (OneShotRunner, WireRunner, ReplRunner).
Locator class shell_app/locate/locator.py Resolves every filesystem path from home/cwd + BRAND. Re-exported as NodeLocator.
BRAND const shell_app/locate/brand.py The frozen source of naming truth (app/bin name, env prefix, dir/file basenames).
load_settings async function shell_app/config/settings.py Merge defaults ← global ← project; never raises for an unreadable layer.
resolve_model_id function shell_app/config/settings.py Choose a model id: invocation override > default_model > claude-sonnet-4.
Settings / DEFAULT_SETTINGS dataclass shell_app/config/settings.py The user-tunable config surface and its minimal baseline.
run_auth_command async function shell_app/auth_cli/oauth_cli.py Entry point for the auth subcommand; returns 0/1/2, never sys.exit.
AuthIO class shell_app/auth_cli/oauth_cli.py The minimal print/warn/ask terminal Protocol the auth command reads/writes through.

Programmatic Use

The whole CLI is callable in-process — useful for embedding or testing without spawning a subprocess.

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)

Assemble a boot context and select a runner by hand, running the teardown closables yourself:

import asyncio, sys
from indusagi.shell_app import (
    tokenize_invocation, build_boot_context, CliStreams, select_runner,
)

async def drive():
    inv = tokenize_invocation(["--print", "hi"], attended=False)
    ctx = await build_boot_context(
        inv, CliStreams(input=sys.stdin, output=sys.stdout, error=sys.stderr)
    )
    try:
        runner = select_runner(inv)        # OneShotRunner accepts mode 'print'
        return await runner.run(ctx)
    finally:
        for close in reversed(list(ctx.closables)):
            await close()

asyncio.run(drive())

Run the auth status subcommand with a scripted AuthIO:

import asyncio
from indusagi.shell_app import run_auth_command

class CaptureIO:
    def __init__(self): self.lines = []
    def print(self, line): self.lines.append(line)
    def warn(self, line): self.lines.append("WARN: " + line)
    async def ask(self, prompt): return ""

io = CaptureIO()
code = asyncio.run(run_auth_command(["status"], io))
print(code, io.lines)

See Also

  • Shell App — the full architecture of the command-line front door.
  • Runtime — the agent the runners drive.
  • LLM Gateway — model cards, the SECRET_TABLE, and OAuth helpers.
  • Interop — MCP server mounting behind --mcp.
  • UI Bridge — the Textual interactive surface mounted in repl mode.
  • Getting Started — install and first run.
  • Package Exports — the full public surface.