§ Language Reference
Python ● stable
Adapters for click, typer, and argparse. One call to murli.enable() adds dual-audience output, runtime introspection, structured errors, and safety rails to any Python CLI.
Installation
shellpip
pip install murli # core + argparse adapter pip install "murli[click]" # + click adapter pip install "murli[typer]" # + typer adapter pip install "murli[all]" # click + typer
Quick start
click
pythoncli.py
import click
import murli
@click.group()
def cli(): pass
murli.enable(cli) # injects --agent, --schema, --force, --dry-run, --output, --profile
# mounts describe, doctor, profile subcommands
@cli.command()
@murli.pass_writer
def deploy(writer):
writer.write_success("Deployed", {"status": "ok"})
if __name__ == "__main__":
cli()
typer
pythoncli.py
import typer
import murli
app = typer.Typer()
murli.enable(app)
@app.command()
def deploy(ctx: typer.Context):
writer = murli.get_writer(ctx)
writer.write_success("Deployed", {"status": "ok"})
if __name__ == "__main__":
app()
argparse
pythoncli.py
import argparse
import murli
parser = argparse.ArgumentParser(description="My tool")
murli.enable(parser)
parser.add_argument("--env", required=True)
args, writer = murli.parse(parser) # drop-in for parse_args()
writer.write_success(f"Deployed to {args.env}", {"env": args.env})
Entry points
| Function | Signature | Purpose |
|---|---|---|
| enable | enable(app) → None | Enable murli on a click.Group, typer.Typer, or argparse.ArgumentParser. Injects flags, mounts subcommands. |
| annotate | annotate(cmd, meta: Metadata) → None | Attach rich metadata to a command for schema and describe output. |
| get_writer | get_writer(ctx) → Writer | Retrieve the Writer from a click or typer context object. |
| parse | parse(parser, args=None) → (Namespace, Writer) | argparse drop-in for parse_args(). Returns the namespace and a configured Writer. |
| @pass_writer | decorator | click decorator — injects writer: Writer as the first argument to the decorated function. |
Writer
Output methods
| Method | TTY | Agent / piped |
|---|---|---|
| write_success(human_text, json_payload) | stdout plain text | {"status":"ok",...} to stdout |
| write_plan(human_text, plan) | stdout plain text | {"status":"plan",...} to stdout |
| write_error(err: AgentError) | stderr plain text + exit | {"status":"error",...} to stderr + exit |
| write_event(v) | no-op | NDJSON line to stdout |
| write_progress(evt: ProgressEvent) | stderr plain text | {"event":"progress",...} to stdout |
| log(msg) | stderr (deduplicated) | stderr (deduplicated) |
State methods
| Method | Returns |
|---|---|
| is_tty() → bool | True if stdout is a terminal |
| is_forced() → bool | True if --force or --yes was passed |
| is_dry_run() → bool | True if --dry-run was passed |
ProgressEvent
python
murli.ProgressEvent(
stage="indexing",
current=500,
total=2000,
percent=25.0,
eta_ms=6000,
message="Indexing files",
)
Metadata
Pass to murli.annotate(). All fields are optional.
python
murli.annotate(deploy_cmd, murli.Metadata(
agent_description="Deploys the application to the target environment.",
when_to_use="Use when the build has passed and artifacts are ready.",
idempotent=False,
mutating=True,
dry_runnable=True,
destructive=False,
returns=murli.ReturnSchema(
description="Deployment result",
type="object",
properties={"env": "string", "version": "string"},
),
examples=[
murli.Example(command="mytool deploy --env prod", description="Deploy to production"),
],
flag_annotations={
"env": murli.FlagAnnotation(enum=["dev", "staging", "prod"], env="MYTOOL_ENV"),
"token": murli.FlagAnnotation(sensitive=True, env="MYTOOL_TOKEN"),
},
))
Metadata fields
| Field | Type | Purpose |
|---|---|---|
| agent_description | str | Description optimised for agent consumption |
| when_to_use | str | Guidance on when to choose this command over alternatives |
| idempotent | bool | True if calling multiple times yields the same result |
| mutating | bool | True if the command changes state. Activates the confirmation guard in non-interactive mode. |
| dry_runnable | bool | True if --dry-run is supported |
| destructive | bool | True if the command deletes or irreversibly modifies data |
| returns | ReturnSchema | None | Shape and description of the JSON payload |
| examples | list[Example] | Worked examples with command string and description |
| flag_annotations | dict[str, FlagAnnotation] | Per-flag metadata. Key is the flag name. |
FlagAnnotation fields
| Field | Type | Effect |
|---|---|---|
| env | str | Environment variable that sets this flag |
| sensitive | bool | Value redacted in schema output and logs |
| persistent | bool | Flag applies to all subcommands |
| enum | list[str] | Valid values. Surfaced in schema and error envelopes. |
| pattern | str | Regex the value must match (informational) |
| mutually_exclusive_with | list[str] | Flag names that cannot be set simultaneously |
| profileable | bool | Flag can be saved and recalled via named profiles |
AgentError
Raise an AgentError to write a structured error envelope to stderr and exit with the appropriate code.
Convenience constructors
python
raise murli.AgentError.user_error("query string cannot be empty", "Provide a search keyword.")
raise murli.AgentError.tool_error("database connection failed: timeout after 30s")
raise murli.AgentError.not_found("index not found at ~/.mytool/index", "Run `mytool index build`.")
raise murli.AgentError.rate_limited("API rate limit hit", retry_after_ms=5000)
Full constructor
python
raise murli.AgentError(
code=murli.EXIT_NOT_FOUND,
error_type="index_missing",
message="Semantic index not found at ~/.mytool/index",
suggestion="Run `mytool index build` to create the index first.",
recoverable=False,
doc_url="https://example.com/docs/indexing",
)
Profiles
The profile subcommand is auto-mounted by murli.enable(). Mark any flag as profileable=True in its FlagAnnotation to include it in profile operations.
shell
# Save flags as a named profile $ mytool profile set production env=prod token=abc123 # Use a profile by default $ mytool profile set-default production # Override for a single invocation $ mytool --profile staging deploy
Profile subcommands: list, set, delete, set-default, show. Profiles are stored in platformdirs.user_config_dir(tool_name) / "profiles.json".
Differences from the Go implementation
| Feature | murli-go | murli-py |
|---|---|---|
| Profile storage | ~/. | platformdirs.user_config_dir (platform-appropriate path) |
| Profile save command | profile save / profile use | profile set / profile set-default |
| Mutation guard scope | Per-command (cobra wraps all) | Per leaf command (Groups are routers, not guards) |
| Context cancellation | Auto-mapped from context.Canceled | Not applicable (synchronous Python) |