Skip to content

Extension CLI Entry xune

Besides attaching tools, commands, and hooks to an agent, an extension can declare a command line of its own and have xune run it directly. This fits operational work that has nothing to do with the agent but everything to do with extension data: reports, data backfills, budget settings.

xune                       # print usage and list extensions that declare a CLI
xune <extension> [args…]   # run that extension's CLI
xune <extension> -h        # that extension's own --help
xune stat show --days 7
xune stat budget set --day 500 --user alice

Arguments and exit codes

xune has a single positional argument — the extension name. Every other argument is handed verbatim to the extension's own parser.

Case Output Exit code
no arguments usage + list of extensions with a CLI 2
-h / --help same as above 0
extension not loaded unknown extension '<name>' — loaded: … 2
extension declares no CLI extension '<name>' declares no CLI 2
normal execution decided by the extension the handler's int return value is the exit code, otherwise 0

How it finds the extension

xune reuses the core extension loader: it scans $XUN_HOME/extensions/ and imports the extension modules, then reads the parser and handler declared by @extension_attr(cli=(parser, handler)):

flowchart LR
    A["xune &lt;name&gt; args"] --> B["Scan and import<br/>$XUN_HOME/extensions"]
    B --> C{"Does this extension declare cli?"}
    C -- no --> X["Print a hint and exit 2"]
    C -- yes --> D["parser.parse_args(args)"]
    D --> E["handler(namespace)"]
    E --> F["Return value is the exit code"]

Two points are worth noting:

  • Import only, no setup. xune never runs setup_extension(ctx), so nothing is attached to any agent; resources that need initialization (creating a database, creating directories) must be handled inside the CLI handler itself.
  • Same data directory rules. Because the core process is the one importing the extension, extension_data_dir() resolves to the same path an agent sees, so no extra PYTHONPATH or module search path is required.

How to declare it

cli is a two-tuple (argparse.ArgumentParser, handler), and the handler receives the parsed Namespace:

import argparse
from xun import ExtensionContext, extension_attr

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser()
    parser.add_argument("--days", type=int, default=7)
    return parser

def run(args) -> int:
    print(f"last {args.days} days")
    return 0

@extension_attr(api_min_version="1.4.0", data_version="1", cli=(build_parser(), run))
def setup_extension(ctx: ExtensionContext) -> None:
    ...

The parser's prog is rewritten to xune <name>, so --help shows exactly the command users type. The built-in stat extension uses this mechanism to provide its report and budget commands (xune stat show|budget …); see Extension Attributes.