Skip to content

Cli

cli

AXM-AST CLI entry point โ€” AST introspection for AI agents.

Usage::

axm-ast describe src/mylib
axm-ast describe src/mylib --detail detailed --json
axm-ast inspect src/mylib --symbol MyClass
axm-ast inspect src/mylib --symbol MyClass --source --json
axm-ast graph src/mylib --format mermaid
axm-ast search src/mylib --returns str
axm-ast version

callees(path='.', *, symbol, json_output=False)

Find all functions/methods called by a given symbol.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def callees(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package directory"),
    ] = ".",
    *,
    symbol: Annotated[
        str,
        cyclopts.Parameter(
            name=["--symbol", "-s"],
            help="Symbol name to find callees of",
        ),
    ],
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Find all functions/methods called by a given symbol."""
    project_path = _resolve_dir(path)

    pkg = get_package(project_path)

    from axm_ast.core.flows import find_callees

    results = find_callees(pkg, symbol)

    if json_output:
        print(
            json.dumps(
                [r.model_dump(mode="json") for r in results],
                indent=2,
            )
        )
    elif not results:
        print(f"๐Ÿ“ญ No callees found for '{symbol}'")
    else:
        print(f"๐Ÿ“ž {len(results)} callee(s) of '{symbol}':\n")
        for r in results:
            ctx = f" in {r.context}()" if r.context else ""
            print(f"  {r.module}:{r.line} โ†’ {r.symbol}{ctx}")
            print(f"    {r.call_expression}")

callers(path='.', *, symbol, json_output=False)

Find all call-sites of a given symbol across a package.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def callers(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package or workspace directory"),
    ] = ".",
    *,
    symbol: Annotated[
        str,
        cyclopts.Parameter(
            name=["--symbol", "-s"],
            help="Symbol name to search for callers of",
        ),
    ],
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Find all call-sites of a given symbol across a package."""
    project_path = _resolve_dir(path)

    from axm_ast.core.workspace import detect_workspace

    ws = detect_workspace(project_path)
    if ws is not None:
        from axm_ast.core.callers import find_callers_workspace
        from axm_ast.core.workspace import analyze_workspace

        ws = analyze_workspace(project_path)
        results = find_callers_workspace(ws, symbol)
    else:
        pkg = get_package(project_path)

        from axm_ast.core.callers import find_callers

        results = find_callers(pkg, symbol)

    if json_output:
        print(
            json.dumps(
                [r.model_dump(mode="json") for r in results],
                indent=2,
            )
        )
    elif not results:
        print(f"๐Ÿ“ญ No callers found for '{symbol}'")
    else:
        print(f"๐Ÿ“ž {len(results)} caller(s) of '{symbol}':\n")
        for r in results:
            ctx = f" in {r.context}()" if r.context else ""
            print(f"  {r.module}:{r.line}{ctx}")
            print(f"    {r.call_expression}")

context(path='.', *, json_output=False, depth=None, slim=False)

Dump complete project context in one shot for AI agents.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def context(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package or workspace directory"),
    ] = ".",
    *,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
    depth: Annotated[
        int | None,
        cyclopts.Parameter(
            name=["--depth", "-d"],
            help="Detail level: 0=top-5, 1=sub-packages, 2=modules, 3=symbols",
        ),
    ] = None,
    slim: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--slim"],
            help="Compact output with top modules only (equivalent to --depth 0)",
        ),
    ] = False,
) -> None:
    """Dump complete project context in one shot for AI agents."""
    project_path = _resolve_dir(path)

    # --slim is shorthand for --depth 0
    if slim:
        depth = 0

    from axm_ast.core.workspace import detect_workspace

    ws = detect_workspace(project_path)
    if ws is not None:
        _print_workspace_context(project_path, json_output=json_output)
        return

    from axm_ast.core.context import (
        build_context as _build_context,
    )
    from axm_ast.core.context import (
        format_context,
        format_context_json,
    )

    ctx = _build_context(project_path)

    if json_output:
        print(json.dumps(format_context_json(ctx, depth=depth), indent=2))
    elif depth is not None:
        _print_compact_context(format_context_json(ctx, depth=depth))
    else:
        print(format_context(ctx))

dead_code(path='.', *, json_output=False, include_tests=False)

Detect dead (unreferenced) code in a Python package.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command(name="dead-code")
def dead_code(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package directory"),
    ] = ".",
    *,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
    include_tests: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--include-tests"],
            help="Include test fixtures in scan",
        ),
    ] = False,
) -> None:
    """Detect dead (unreferenced) code in a Python package."""
    project_path = _resolve_dir(path)

    from axm_ast.core.dead_code import find_dead_code, format_dead_code

    pkg = get_package(project_path)
    results = find_dead_code(pkg, include_tests=include_tests)

    if json_output:
        print(
            json.dumps(
                {
                    "dead_symbols": [
                        {
                            "name": d.name,
                            "module_path": d.module_path,
                            "line": d.line,
                            "kind": d.kind,
                        }
                        for d in results
                    ],
                    "total": len(results),
                },
                indent=2,
            )
        )
    else:
        print(format_dead_code(results))

describe(path='.', *, detail='detailed', json_output=False, budget=None, rank=False, compress=False, modules=None)

Describe a Python package at the chosen detail level.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def describe(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package/module directory"),
    ] = ".",
    *,
    detail: Annotated[
        str,
        cyclopts.Parameter(
            name=["--detail", "-d"],
            help="Detail level: summary, detailed, full",
        ),
    ] = "detailed",
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
    budget: Annotated[
        int | None,
        cyclopts.Parameter(
            name=["--budget", "-b"],
            help="Max output lines (truncate intelligently)",
        ),
    ] = None,
    rank: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--rank"],
            help="Sort symbols by importance (PageRank)",
        ),
    ] = False,
    compress: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--compress"],
            help="Compressed output: signatures + docstring summaries",
        ),
    ] = False,
    modules: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--modules", "-m"],
            help="Comma-separated module name filters (substring, case-insensitive)",
        ),
    ] = None,
) -> None:
    """Describe a Python package at the chosen detail level."""
    project_path = _resolve_dir(path)

    from axm_ast.formatters import filter_modules, format_toc

    pkg = get_package(project_path)

    # Apply module filter
    mod_filter = [m.strip() for m in modules.split(",")] if modules else None
    pkg = filter_modules(pkg, mod_filter)

    if detail == "toc":
        _print_toc(format_toc(pkg), json_output=json_output)
        return

    if compress:
        from axm_ast.formatters import format_compressed

        print(format_compressed(pkg))
    elif json_output:
        print(json.dumps(format_json(pkg, detail=detail), indent=2))
    else:
        print(format_text(pkg, detail=detail, budget=budget, rank=rank))

diff_cmd(refs, path='.', *, json_output=False)

Structural diff between two branches at symbol level.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command(name="diff")
def diff_cmd(
    refs: Annotated[
        str,
        cyclopts.Parameter(help="Git refs in base..head format"),
    ],
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package directory"),
    ] = ".",
    *,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Structural diff between two branches at symbol level."""
    if ".." not in refs:
        print("โŒ Expected format: base..head (e.g. main..feature)", file=sys.stderr)
        raise SystemExit(1)

    parts = refs.split("..", 1)
    base, head = parts[0], parts[1]
    if not base or not head:
        print("โŒ Both base and head refs are required", file=sys.stderr)
        raise SystemExit(1)

    project_path = _resolve_dir(path)

    from axm_ast.core.structural_diff import structural_diff

    result = structural_diff(project_path, base, head)

    if "error" in result:
        print(f"โŒ {result['error']}", file=sys.stderr)
        raise SystemExit(1)

    if json_output:
        print(json.dumps(result, indent=2))
    else:
        _print_diff(result, base, head)

docs(path='.', *, detail='full', pages_filter=None, json_output=False, tree_only=False)

Dump project documentation tree and content in one shot.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def docs(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Project root directory"),
    ] = ".",
    *,
    detail: Annotated[
        str,
        cyclopts.Parameter(
            name=["--detail", "-d"],
            help="Detail level: toc, summary, full",
        ),
    ] = "full",
    pages_filter: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--pages", "-p"],
            help="Comma-separated page name substrings to filter",
        ),
    ] = None,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
    tree_only: Annotated[
        bool,
        cyclopts.Parameter(name=["--tree"], help="Only show directory tree"),
    ] = False,
) -> None:
    """Dump project documentation tree and content in one shot."""
    project_path = _resolve_dir(path)

    from axm_ast.core.docs import discover_docs, format_docs, format_docs_json

    pages = [p.strip() for p in pages_filter.split(",")] if pages_filter else None
    result = discover_docs(project_path, detail=detail, pages=pages)

    if json_output:
        print(json.dumps(format_docs_json(result), indent=2))
    else:
        print(format_docs(result, tree_only=tree_only))

flows(path='.', *, trace=None, max_depth=5, cross_module=False, detail='trace', no_exclude_stdlib=False, json_output=False)

Detect entry points and trace execution flows.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def flows(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package directory"),
    ] = ".",
    *,
    trace: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--trace", "-t"],
            help="Entry point name to trace flow from",
        ),
    ] = None,
    max_depth: Annotated[
        int,
        cyclopts.Parameter(
            name=["--max-depth"],
            help="Maximum BFS depth for flow tracing",
        ),
    ] = 5,
    cross_module: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--cross-module"],
            help="Resolve imports and trace into external modules",
        ),
    ] = False,
    detail: Annotated[
        str,
        cyclopts.Parameter(
            name=["--detail", "-d"],
            help="Detail level: trace (default) or source (include function source)",
        ),
    ] = "trace",
    no_exclude_stdlib: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--no-exclude-stdlib"],
            help="Include stdlib/builtin callees in flow trace",
        ),
    ] = False,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Detect entry points and trace execution flows."""
    project_path = _resolve_dir(path)

    from axm_ast.core.flows import (
        find_entry_points,
        format_flows,
        trace_flow,
    )

    pkg = get_package(project_path)

    if trace is not None:
        steps = trace_flow(
            pkg,
            trace,
            max_depth=max_depth,
            cross_module=cross_module,
            detail=detail,
            exclude_stdlib=not no_exclude_stdlib,
        )
        if json_output:
            print(
                json.dumps(
                    {
                        "entry": trace,
                        "steps": [s.model_dump(mode="json") for s in steps],
                        "count": len(steps),
                    },
                    indent=2,
                )
            )
        elif not steps:
            print(f"๐Ÿ“ญ No flow found for '{trace}'")
        else:
            print(f"๐Ÿ”€ Flow from '{trace}' ({len(steps)} step(s)):\n")
            for s in steps:
                indent = "  " * s.depth
                print(f"  {indent}{s.name} ({s.module}:{s.line})")
        return

    entries = find_entry_points(pkg)
    if json_output:
        print(
            json.dumps(
                {
                    "entry_points": [e.model_dump(mode="json") for e in entries],
                    "count": len(entries),
                },
                indent=2,
            )
        )
    else:
        print(format_flows(entries))

graph(path='.', *, fmt='text', json_output=False)

Display the internal import/dependency graph.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def graph(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package or workspace directory"),
    ] = ".",
    *,
    fmt: Annotated[
        str,
        cyclopts.Parameter(
            name=["--format", "-f"],
            help="Output format: text, mermaid, json",
        ),
    ] = "text",
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Display the internal import/dependency graph."""
    project_path = _resolve_dir(path)

    from axm_ast.core.workspace import detect_workspace

    ws = detect_workspace(project_path)
    if ws is not None:
        _print_workspace_graph(project_path, fmt=fmt, json_output=json_output)
        return

    _print_package_graph(project_path, fmt=fmt, json_output=json_output)

impact(path='.', *, symbol, json_output=False, exclude_tests=False)

Analyze the impact of changing a symbol.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def impact(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package or workspace directory"),
    ] = ".",
    *,
    symbol: Annotated[
        str,
        cyclopts.Parameter(
            name=["--symbol", "-s"],
            help="Symbol name to analyze impact for",
        ),
    ],
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
    exclude_tests: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--exclude-tests"],
            help="Filter test callers from impact output",
        ),
    ] = False,
) -> None:
    """Analyze the impact of changing a symbol."""
    project_path = _resolve_dir(path)

    from axm_ast.core.workspace import detect_workspace

    ws = detect_workspace(project_path)
    if ws is not None:
        from axm_ast.core.impact import analyze_impact_workspace

        result = analyze_impact_workspace(
            project_path, symbol, exclude_tests=exclude_tests
        )
    else:
        from axm_ast.core.impact import analyze_impact

        result = analyze_impact(project_path, symbol, exclude_tests=exclude_tests)

    if json_output:
        print(json.dumps(result, indent=2))
    else:
        _print_impact(result)

inspect(path='.', *, symbol=None, source=False, json_output=False)

Inspect a symbol by name across a package.

Operates on packages (not individual files). Supports dotted paths like ClassName.method or module.symbol. Returns file path, line numbers, and optionally source code โ€” matching MCP ast_inspect.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def inspect(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package directory"),
    ] = ".",
    *,
    symbol: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--symbol", "-s"],
            help="Symbol name to inspect (supports dotted paths like Class.method)",
        ),
    ] = None,
    source: Annotated[
        bool,
        cyclopts.Parameter(
            name=["--source"],
            help="Include source code in output",
        ),
    ] = False,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Inspect a symbol by name across a package.

    Operates on packages (not individual files). Supports dotted paths
    like ``ClassName.method`` or ``module.symbol``. Returns file path,
    line numbers, and optionally source code โ€” matching MCP ``ast_inspect``.
    """
    project_path = _resolve_dir(path)

    if not symbol:
        # List all symbols in the package
        pkg = get_package(project_path)
        symbols = search_symbols(pkg, name=None, returns=None, kind=None, inherits=None)
        if json_output:
            print(
                json.dumps(
                    {"symbols": [s.model_dump(mode="json") for s in symbols]},
                    indent=2,
                )
            )
        else:
            for s in symbols:
                sig = getattr(s, "signature", None)
                if sig:
                    print(f"  ยท {sig}")
                else:
                    print(f"  ยท class {s.name}")
        return

    from axm_ast.tools.inspect import InspectTool

    tool = InspectTool()
    result = tool.execute(path=str(project_path), symbol=symbol, source=source)

    if not result.success:
        print(f"โŒ {result.error}", file=sys.stderr)
        raise SystemExit(1)

    sym_data = result.data["symbol"]
    if json_output:
        print(json.dumps(sym_data, indent=2))
    else:
        _print_inspect_result(sym_data)

main()

Main entry point.

Source code in packages/axm-ast/src/axm_ast/cli.py
def main() -> None:
    """Main entry point."""
    app()

search(path='.', *, name=None, returns=None, kind=None, inherits=None, json_output=False)

Search for symbols across a package with filters.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def search(
    path: Annotated[
        str,
        cyclopts.Parameter(help="Path to package directory"),
    ] = ".",
    *,
    name: Annotated[
        str | None,
        cyclopts.Parameter(name=["--name", "-n"], help="Search by symbol name"),
    ] = None,
    returns: Annotated[
        str | None,
        cyclopts.Parameter(name=["--returns", "-r"], help="Filter by return type"),
    ] = None,
    kind: Annotated[
        str | None,
        cyclopts.Parameter(
            name=["--kind", "-k"],
            help=(
                "Filter by kind: function, method, property,"
                " classmethod, staticmethod, abstract, class"
            ),
        ),
    ] = None,
    inherits: Annotated[
        str | None,
        cyclopts.Parameter(name=["--inherits"], help="Filter classes by base class"),
    ] = None,
    json_output: Annotated[
        bool,
        cyclopts.Parameter(name=["--json"], help="Output as JSON"),
    ] = False,
) -> None:
    """Search for symbols across a package with filters."""
    project_path = _resolve_dir(path)

    pkg = get_package(project_path)

    kind_enum = SymbolKind(kind) if kind else None
    results = search_symbols(
        pkg, name=name, returns=returns, kind=kind_enum, inherits=inherits
    )

    if not results:
        print("No results found.")
        return

    if json_output:
        print(json.dumps([r.model_dump(mode="json") for r in results], indent=2))
    else:
        print(f"๐Ÿ” {len(results)} result(s):\n")
        for r in results:
            if hasattr(r, "signature"):
                print(f"  ยท {r.signature}")
            else:
                print(f"  ยท class {r.name}")

version()

Show axm-ast version.

Source code in packages/axm-ast/src/axm_ast/cli.py
@app.command()
def version() -> None:
    """Show axm-ast version."""
    from axm_ast import __version__

    print(f"axm-ast {__version__}")