Skip to main content

Fireargs

Define each command with typed input and output models. Get composability, LLM-ready manifests, and structured JSON I/O baked in — for humans, agents, and scripts alike. Built on pydantic and click; Python 3.12+.

Installation

pip install fireargs
# or
uv add fireargs

A first command

The handler's first parameter annotation is the input model and its return annotation the output type. Fields become arguments and options; Argument(...) and Option(...) are pydantic Field()s that also carry CLI config. Import the names you use, as here, or the module (import fireargs as fa, then fa.Option(...)).

from typing import Annotated
from pydantic import BaseModel
from fireargs import Argument, Option, command


# Composable arguments and options
Name = Annotated[str, Argument(description="The user's name.")]
Times = Annotated[int, Option(default=1, ge=1, description="Repeat it n times.")]
Shout = Annotated[bool, Option(short="s", default=False, description="Use capitals.")]


# Typed input, validated on every run
class GreetInput(BaseModel):
    name: Name
    times: Times
    shout: Shout


# Typed output, validated on every run
class GreetOutput(BaseModel):
    greeting: str


# A click command, typed from the handler's annotations
@command(description="Greet someone politely.")
def greet(input: GreetInput) -> GreetOutput:
    greeting = " ".join([f"hello {input.name}"] * input.times)
    return GreetOutput(greeting=greeting.upper() if input.shout else greeting)


# Output for humans; --json prints the data instead
@greet.render
def render_greeting(output: GreetOutput) -> str:
    return output.greeting


# Use as a normal click command, e.g. configure or apply plugins
if __name__ == "__main__":
    greet()

That gets you:

$ python greet.py world --times 2 -s        # CLI input, rendered output
HELLO WORLD HELLO WORLD
$ python greet.py world --json              # CLI input, JSON output
{
  "greeting": "hello world"
}
$ python greet.py --json '{"name":"world","times":2}'   # JSON input, JSON output
{
  "greeting": "hello world hello world"
}
$ python greet.py world --times 0
error: option '--times <value>' argument '0' is invalid. Input should be greater than or equal to 1

--help comes from click:

Usage: greet.py [OPTIONS] <name>

  Greet someone politely.

Arguments:
  name  The user's name.

Options:
  --times <value>  Repeat it n times.  [default: 1]
  -s, --shout      Use capitals.
  --json [value]   write output as JSON; pass a JSON string to also drive
                   input
  --llms           print this command's schema (name, input, output) as JSON
                   for LLM tool-use
  --help           Show this message and exit.

A first program

Group leaves with Program(router={...}):

# Composable commands: a router of leaves and nested programs
cli = Program(
    name="myapp",
    description="My CLI",
    version="1.0.0",
    router={"greet": greet, "deploy": deploy, "db": db},
)

# Use as a normal click group, e.g. configure or apply plugins
if __name__ == "__main__":
    cli()

That gives:

$ myapp --help
Usage: myapp [OPTIONS] [COMMAND] [ARGS]...

  My CLI

Options:
  -V, --version  output the version number
  --llms         print the program manifest (subcommand schemas) as JSON for
                 LLM tool-use
  --help         Show this message and exit.

Commands:
  greet   Greet someone politely.
  deploy  Deploy the current build.
  db      Database tasks.

$ myapp db migrate --json '{}'
{
  "applied": 0
}

Programs nest — router accepts other programs the same way it accepts leaves. myapp db migrate routes through both levels.

LLMs mode

Every command and program ships with --llms, which prints an MCP-tools/list-shaped manifest. For greet.py above:

{
  "tools": [
    {
      "name": "help",
      "description": "Documentation tool, not callable. Each `tools[].name` is the space-separated path beneath this binary. Invoke a tool with `<binary> <name> --json '<value>'` matching that tool's `inputSchema`; output is JSON on stdout matching `outputSchema`.",
      "inputSchema": { "type": "object", "properties": {} },
      "outputSchema": { "type": "object", "properties": {} }
    },
    {
      "name": "greet",
      "inputSchema": {
        "properties": {
          "name": {
            "description": "The user's name.",
            "title": "Name",
            "type": "string"
          },
          "times": {
            "default": 1,
            "description": "Repeat it n times.",
            "minimum": 1,
            "title": "Times",
            "type": "integer"
          },
          "shout": {
            "default": false,
            "description": "Use capitals.",
            "title": "Shout",
            "type": "boolean"
          }
        },
        "required": ["name"],
        "title": "GreetInput",
        "type": "object"
      },
      "outputSchema": {
        "properties": { "greeting": { "title": "Greeting", "type": "string" } },
        "required": ["greeting"],
        "title": "GreetOutput",
        "type": "object"
      },
      "description": "Greet someone politely."
    }
  ]
}

An LLM agent reads the manifest, picks a tool, and invokes it with <binary> <name> --json '<value>'. Output comes back as JSON on stdout.

JSON mode

Every command takes --json. Bare, it switches the output to JSON; given a value, that value is the input, checked against the input schema, so scripts never assemble argv:

$ python greet.py world --times 2 --json        # CLI input, JSON output
{
  "greeting": "hello world hello world"
}
$ python greet.py --json '{"name":"world","times":3,"shout":true}'   # JSON input, JSON output
{
  "greeting": "HELLO WORLD HELLO WORLD HELLO WORLD"
}
$ python greet.py world --json | jq -r .greeting   # pipe it into other tools
hello world
$ python greet.py --json '{"times":2}'           # input errors go to stderr, exit code 1
error: invalid --json input at 'name': Field required
$ python greet.py --json '{"name":"world","times":"2"}'
error: invalid --json input at 'times': Input should be a valid integer

stdout only ever carries the data, so a script can parse it without checking for noise.

Learn more

  • Commands — the @command(...) decorator and the leaf it builds.
  • Inputs — Argument() / Option() and per-field click config.
  • Outputs — the output type, serialization, and JSON Schema in --llms.
  • Programs — subcommand trees, nesting, and program-level config.
  • LLMs mode — --llms and the MCP-shaped manifest.
  • JSON mode — --json for programmatic and pipeline use.
  • Composability — share common params across commands by composing pydantic models.
  • Agent Skills — teach your coding agent to write and drive Fireargs CLIs.
  • Contributing — local setup, conventions, release flow.

License

MIT.

Metadata

Release files for fireargs 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fireargs 0.1.2
File Size Uploaded Provenance Actions
fireargs-0.1.2.tar.gz 29.9 kB Without provenance Details for fireargs-0.1.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for fireargs 0.1.2
File Interpreter ABI Platform Actions
fireargs-0.1.2-py3-none-any.whl Python 3 none any Details for fireargs-0.1.2-py3-none-any.whl

Total release size: 71.4 kB

Release files / fireargs-0.1.2.tar.gz

Download URL fireargs-0.1.2.tar.gz
Size 29.9 kB
Tags Source
SHA-256 checksum
How to use checksums
56820db07d837cf69270df724b274845c2d63dcb90d9099812898b3a59ffa88d
BLAKE2b-256 checksum
How to use checksums
1472c12a663d8a5ee89d6c3e3ac23ea0f5db78648e277ac17184bfbfc8657173
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fireargs-0.1.2-py3-none-any.whl

Download URL fireargs-0.1.2-py3-none-any.whl
Size 41.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
97e3ebbbaf7c5b065ed9e4d9ef905bd0f5b653c3250830f025cd15d238ceb09d
BLAKE2b-256 checksum
How to use checksums
dcefb46d10902609334cacab5c84a70a11bb0e259bc38bc72534692cb2f853b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for fireargs 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page