Skip to main content

agentcli

Shared conventions for command-line tools whose primary callers are agents. It owns no food domain: it owns predictable errors, JSON output, skills, in-binary guides, and the candidate record used for composition.

Use it in a package

Install the distribution (the import name remains agentcli):

uv add click-agentcli

Register the shared output, guide, and skill commands on a Click CLI:

import click

from agentcli import (
    JsonAwareGroup,
    emit,
    guide_command,
    json_option,
    skill_group,
)

GUIDE = """# acme guide

Use `acme hello` to print a greeting.
"""


@click.group(cls=JsonAwareGroup)
def cli() -> None:
    """Acme's agent-facing CLI."""


@cli.command()
@json_option
def hello(json_output: bool) -> None:
    """Print a greeting."""
    emit(
        {"message": "hello"},
        json_output=json_output,
        human=lambda result: [result["message"]],
    )


cli.add_command(guide_command(GUIDE))
cli.add_command(skill_group(name="acme", package="acme"))

The command has readable output for people and one stable document for agents:

$ acme hello
hello
$ acme hello --json
{"ok":true,"data":{"message":"hello"}}
$ acme guide
# acme guide
...
$ acme skill install
copied  /home/me/.agents/skills/acme

Declare the entry point and ship the skill inside the import package. For Hatchling, a project-root SKILL.md can be mapped into the required wheel location like this:

[project.scripts]
acme = "acme.cli:cli"

[tool.hatch.build.targets.wheel.force-include]
"SKILL.md" = "acme/skills/acme/SKILL.md"

skill_group(name="acme", package="acme") expects an installed wheel to contain acme/skills/acme/SKILL.md. The same file may stay at the repository root for source-checkout use. Its frontmatter name must match the skill name:

---
name: acme
description: Use Acme from an agent.
---

Run `acme guide` for the complete manual.

Add @json_option to each command that supports structured output and call emit once with both the data and its human renderer. JsonAwareGroup then keeps parse failures structured when --json was requested.

Upgrading

Upgrading the package is the whole workflow:

uv tool upgrade --all

JsonAwareGroup rewrites any installed copy of the skill that differs from the packaged one, on the way into a command. It only ever rewrites a directory that already holds this skill: putting one somewhere new is skill install. Failures are silent, and skill itself is skipped so skill status still reports drift.

Set AGENTCLI_NO_SKILL_REFRESH=1 to switch it off. A top-level group that is not JsonAwareGroup asks for the refresh itself:

from agentcli import refresh_skill

refresh_skill(name="acme", package="acme")

Develop this package

uv sync --project .
uv run --project . pytest -q

CLI contract

Every consuming tool uses click, declares --json per command with json_option, and makes its top-level group JsonAwareGroup. The group scans raw arguments so even parse failures that happen before a subcommand exists honour a --json request. Importing agentcli.exits also changes Click's own usage-error code from 2 to 1; consumers must not repeat that correction.

code meaning
0 success
1 usage error or a caller-liftable refusal
2 remote, network, or site failure after allowed retries
3 a caller-stated assertion did not hold
4 a data-quality warning escalated by --strict

An exhausted request budget is code 1, because the caller can lift it. A proportional recipe fit with no solution is code 3.

--json emits exactly one JSON object on stdout and nothing else. Success and failure are symmetric:

{"ok":true,"data":{}}
{"ok":false,"error":{"message":"..."}}

A search with no matches is successful with an empty list. Under --json, errors go to stdout so a caller never has to merge streams to recover the one promised document. Human errors go to stderr.

The stable public surface is:

  • UsageError, RemoteError, AssertionFailure, and StrictFailure.
  • dumps, emit, emit_error, json_option, and limit_option.
  • JsonAwareGroup for every consuming tool's top-level group. It also keeps the installed skill in step with the packaged one.
  • refresh_skill(name=..., package=...) to do that from another group.
  • skill_group(name=..., package=...) for skill install, uninstall, and status. Installation refuses an unrelated destination, recognises owned broken symlinks an older version left, always copies, and supports --to and --dry-run. status compares each installed copy against the packaged one and reports current or stale. It never links: a link points into the environment, whose path carries the interpreter version, so a rebuild elsewhere leaves the skill silently absent rather than merely stale. With no options it installs everywhere the skill is wanted and refreshes its own earlier copies, so plain install is the whole job; a directory holding somebody else's skill is still refused.
  • guide_command(text) for a complete manual available without a network.
  • candidate, macro_options, matches, rank, and unverifiable for the shared composition record and filters below.

Candidate contract

Candidate sources answer the same question: filter things someone could eat by per-serving figures, then rank them with provenance. Recipes and restaurant meals therefore emit the same record:

{
  "kind":"recipe",
  "id":"sourdough-pizza",
  "name":"Sourdough Pizza",
  "per_serving":{"kcal":384.2,"protein":31.5,"fat":12.1,"carbs":38.4},
  "complete":true,
  "detail":{}
}

kind is recipe or meal. id is accepted back by the emitting tool; display-only slugs are not identifiers. Source-specific fields live under detail, which shared code never reads.

Sources accept macro_options (--max-kcal, --min-protein) and use rank. The rank key is unrounded protein per 100 kcal, then absolute protein, then name. --max-kcal 0 is valid because zero-calorie records exist.

per_serving contains only figures actually known by the source, whatever they are — a source that also publishes fibre or sodium puts them here. Missing is never filled with zero. candidate takes the keys that count as a full set from its caller as required, because this package does not know what a macro is; callers answering the same question share that tuple so complete keeps meaning the same thing across them. A candidate missing a requested filter figure is excluded and returned in the source's unverifiable or equivalent bucket. Every source emits that bucket, even when its loader makes it structurally empty.

This contract is the reason the tools can be independent packages: an orchestrator can merge and rank results without knowing which source answered.

Metadata

Release files for click-agentcli 0.5.0

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

Source distribution (sdist)

Source distribution for click-agentcli 0.5.0
File Size Uploaded Provenance
click_agentcli-0.5.0.tar.gz 28.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for click-agentcli 0.5.0
File Interpreter ABI Platform
click_agentcli-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 58.2 kB

Release files / click_agentcli-0.5.0.tar.gz

Download URL click_agentcli-0.5.0.tar.gz
Size 28.5 kB
Tags Source
SHA-256 checksum
How to use checksums
0140851d505d10c7ea871595f378a9131ac0b7210bc53925e5590470a5cb6c2e
BLAKE2b-256 checksum
How to use checksums
c395d67b0431b9f8673b31cf057f6e640fc80b92d2a8dc2f7cb9bbb0cc8dbfdf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / click_agentcli-0.5.0-py3-none-any.whl

Download URL click_agentcli-0.5.0-py3-none-any.whl
Size 29.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3a7ff557bd65bb583ee499eb720b9aef1b724d185260234b421399202214e020
BLAKE2b-256 checksum
How to use checksums
e5d38db99f36e9152a52d8ef9ba03ab38f351ada328c5cc8d246d780b2d6b0a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Security for click-agentcli 0.5.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for click_agentcli-0.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

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.

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