murliv1.0.2
{ } github
§ 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

FunctionSignaturePurpose
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

MethodTTYAgent / 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-opNDJSON line to stdout
write_progress(evt: ProgressEvent)stderr plain text{"event":"progress",...} to stdout
log(msg)stderr (deduplicated)stderr (deduplicated)

State methods

MethodReturns
is_tty() → boolTrue if stdout is a terminal
is_forced() → boolTrue if --force or --yes was passed
is_dry_run() → boolTrue 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

FieldTypePurpose
agent_descriptionstrDescription optimised for agent consumption
when_to_usestrGuidance on when to choose this command over alternatives
idempotentboolTrue if calling multiple times yields the same result
mutatingboolTrue if the command changes state. Activates the confirmation guard in non-interactive mode.
dry_runnableboolTrue if --dry-run is supported
destructiveboolTrue if the command deletes or irreversibly modifies data
returnsReturnSchema | NoneShape and description of the JSON payload
exampleslist[Example]Worked examples with command string and description
flag_annotationsdict[str, FlagAnnotation]Per-flag metadata. Key is the flag name.

FlagAnnotation fields

FieldTypeEffect
envstrEnvironment variable that sets this flag
sensitiveboolValue redacted in schema output and logs
persistentboolFlag applies to all subcommands
enumlist[str]Valid values. Surfaced in schema and error envelopes.
patternstrRegex the value must match (informational)
mutually_exclusive_withlist[str]Flag names that cannot be set simultaneously
profileableboolFlag 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

Featuremurli-gomurli-py
Profile storage~/./profiles.jsonplatformdirs.user_config_dir (platform-appropriate path)
Profile save commandprofile save / profile useprofile set / profile set-default
Mutation guard scopePer-command (cobra wraps all)Per leaf command (Groups are routers, not guards)
Context cancellationAuto-mapped from context.CanceledNot applicable (synchronous Python)