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, andStrictFailure.dumps,emit,emit_error,json_option, andlimit_option.JsonAwareGroupfor 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=...)forskill install,uninstall, andstatus. Installation refuses an unrelated destination, recognises owned broken symlinks an older version left, always copies, and supports--toand--dry-run.statuscompares each installed copy against the packaged one and reportscurrentorstale. 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 plaininstallis 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, andunverifiablefor 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| click_agentcli-0.5.0.tar.gz | 28.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.