How to Add Custom Command Groups to an Existing CLI-Anything Harness

To add custom command groups to a CLI-Anything harness, define a function decorated with @cli.group() in your <software>_cli.py module, attach sub-commands using @<group>.command(), and run the skill generator to sync the documentation automatically.

CLI-Anything enables rapid construction of AI-agent skills by wrapping software functionality into structured command-line interfaces. When you need to add custom command groups to an existing CLI harness, the framework's source-driven discovery system automatically parses your Python code to update the skill metadata without requiring manual configuration changes.

Understanding the CLI-Anything Discovery Mechanism

The harness discovers command hierarchies by statically analyzing your CLI source code. In zotero/agent-harness/skill_generator.py, the extract_commands_from_cli function uses Python's ast module to parse the <software>_cli.py file and build a metadata model.

The discovery process follows four distinct phases:

  1. AST Parsing – The file is read and parsed into an abstract syntax tree using ast.parse.

  2. Group Identification – The helper _click_decorator_info scans for functions decorated with @cli.group. When the decorator owner is "cli" and the decorator name is "group", the function is registered as a top-level command group. This logic appears in lines 21–25 of skill_generator.py.

  3. Name Resolution – The display name is determined by _default_group_name, which strips trailing _group suffixes, replaces underscores with spaces, and applies title casing. If the decorator includes an explicit name argument, that value takes precedence.

  4. Command Collection – A second pass identifies functions decorated with @<group>.command. The parser resolves the owning group via the group_name_by_function mapping and extracts command names using _default_command_name, while docstrings are captured via ast.get_docstring for descriptions.

Because this metadata extraction is entirely source-driven, adding a new command group requires only modifying the CLI source file—no generator or pipeline updates are necessary.

Step-by-Step Guide to Adding Custom Command Groups

Create the Group Function

Open your <software>_cli.py file and define a new function decorated with @cli.group(). Provide an explicit name if the automatic transformation doesn't meet your needs.

@cli.group()
def report() -> None:
    """Generate high-level reports about the current Library."""
    pass

The docstring becomes the group description in the generated SKILL.md, as retrieved by ast.get_docstring(node) in skill_generator.py (lines 29–31).

Add Sub-Commands to the Group

Define commands under the new group using the @<group>.command("<sub-cmd>") decorator pattern. Each command should include a descriptive docstring.

@report.command("library-summary")
@click.argument("library", required=False)
@click.pass_context
def library_summary(ctx: click.Context, library: str | None) -> int:
    """
    Show a brief summary of a user or group library.
    
    The summary includes the number of collections, items, and tags.
    """
    # Implementation logic here

    return 0

The generator's second pass detects these decorators by checking if the owner name matches a previously recorded group in group_by_display_name, then appends the command to that group's command list (lines 138–145).

Generate Updated Documentation

Run the skill-generation script to verify that the new group appears in the documentation:

python -m cli_anything.zotero generate-skill

This command re-parses zotero_cli.py using the same extract_commands_from_cli logic, ensuring your new report group and its library-summary command appear in the output SKILL.md without additional configuration.

Complete Example: Adding a Report Group to Zotero

Here is a complete implementation extending the Zotero harness with a report command group:


# File: zotero/agent-harness/cli_anything/zotero/zotero_cli.py

import click
from . import catalog, discovery, session_mod

# ... existing code ...

@cli.group()
def report() -> None:
    """Generate high-level reports about the current Library."""
    pass

@report.command("library-summary")
@click.argument("library", required=False)
@click.pass_context
def library_summary(ctx: click.Context, library: str | None) -> int:
    """
    Show a brief summary of a user or group library.

    The summary includes the number of collections, items, and tags.
    """
    runtime = discovery.current_runtime(ctx)
    lib_id = catalog.resolve_library_id(runtime, library) if library else None
    summary = catalog.library_summary(runtime, lib_id)
    # emit function assumed available in scope

    emit(ctx, summary)
    return 0

After adding this code and running python -m cli_anything.zotero generate-skill, the generated SKILL.md will contain:


### Report

Generate high-level reports about the current Library.

| Command | Description |
|---------|-------------|
| library-summary | Show a brief summary of a user or group library. |

How the Generator Recognizes New Groups

The skill_generator.py utility relies on specific AST inspection patterns to identify groups dynamically:

  • Group Detection: The condition owner_name == "cli" and decorator_name == "group" in extract_commands_from_cli identifies your new function as a command group root (source lines 21–25).

  • Decorator Analysis: The _click_decorator_info helper (lines 86–102) dissects decorator chains to resolve the owning object name and the decorator type, distinguishing between @cli.group and @cli.command.

  • Mapping Resolution: The parser maintains a group_name_by_function dictionary that maps function names to their display names. When processing @report.command, the generator resolves owner_name to "report", finds the matching group entry, and appends the command using group_by_display_name[display_name].commands.append(...).

This architecture ensures that any new group defined with the standard Click patterns is automatically incorporated into the skill hierarchy.

Summary

  • Source-driven metadata: CLI-Anything uses AST parsing in skill_generator.py to discover groups decorated with @cli.group() and commands under @<group>.command() without requiring configuration file updates.
  • Simple addition process: Define a function with @cli.group(), add a docstring for the description, implement sub-commands with the group-specific decorator, and run python -m cli_anything.<software> generate-skill.
  • Automatic documentation: The generator creates human-readable SKILL.md files reflecting the current CLI structure, keeping documentation synchronized with code changes.
  • No generator modifications needed: Because extract_commands_from_cli handles discovery generically, you only edit your <software>_cli.py file to extend functionality.

Frequently Asked Questions

Do I need to modify skill_generator.py to add new command groups?

No. The skill_generator.py file is designed to parse any valid Click CLI structure automatically. You only need to add your @cli.group() decorated function to your <software>_cli.py module. The generator's AST parser will detect the new group during the next run of generate-skill.

How does CLI-Anything determine the display name for a command group?

The framework uses the _default_group_name helper function in skill_generator.py. It removes trailing _group suffixes, replaces underscores with spaces, and applies title casing to the function name. Alternatively, you can pass an explicit name to the decorator, such as @cli.group("custom-name"), which overrides the automatic transformation.

Can I nest command groups within other groups?

The current implementation in skill_generator.py primarily discovers top-level groups owned by the main cli object and their immediate sub-commands. While Click supports multi-level group nesting, the standard CLI-Anything harness pattern focuses on flat group structures under the main CLI. For deeply nested hierarchies, you would need to verify that the _click_decorator_info resolver correctly tracks the ownership chain through the group_name_by_function mapping.

Where is the generated documentation stored after running the skill generator?

The generate-skill command outputs a SKILL.md file within the per-skill folder structure, typically located at zotero/agent-harness/cli_anything/zotero/skills/ or the equivalent path for your specific software module. This markdown file contains the structured documentation of all discovered command groups and their associated commands.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →