How OpenHuman CLI Subcommand Dispatch Works and How to Register New Commands

OpenHuman routes CLI arguments through a central dispatch function in src/core/cli.rs that either matches hard-coded top-level commands or falls back to a generic namespace dispatcher, while new commands are registered automatically by adding controller schemas to the domain's schemas.rs and including them in src/core/all.rs.

The OpenHuman command-line interface implements a data-driven architecture where JSON-RPC namespaces surface directly as terminal subcommands. Understanding how run_from_cli_args dispatches execution and how the controller schema system auto-registers methods is essential for extending the toolkit without modifying the core CLI logic.

How the CLI Entry Point Dispatches Commands

The entry point run_from_cli_args in src/core/cli.rs receives the raw argument vector and executes a five-phase dispatch sequence:

  1. Global launch options – parse_launch_options extracts flags such as --model or --provider that affect the entire process.
  2. TUI auto-launch – If the user requests tui/chat or heuristics in should_auto_launch_tui trigger interactive mode, run_tui_from_cli takes over.
  3. Help handling – When no arguments are supplied or the token is --help, print_general_help displays available domains.
  4. Top-level subcommands – A match statement on args[0] handles built-in commands (run, serve, mcp, tui, call, tree-summarizer, memory, agent, sentry-test).
  5. Generic namespace dispatch – Any unrecognized first argument routes to run_namespace_command (lines 1090-1112) for RPC-style execution.
// src/core/cli.rs – main dispatch excerpt
pub fn run_from_cli_args(args: &[String]) -> Result<()> {
    // ... banner printing omitted ...
    match args[0].as_str() {
        "run" | "serve" => run_server_command(&args[1..]),
        "mcp" | "mcp-server" => crate::openhuman::mcp::server::run_stdio_from_cli(&args[1..]),
        "tui" | "chat" => run_tui_from_cli(&args[1..]),
        "call" => run_call_command(&args[1..]),
        "tree-summarizer" => crate::openhuman::memory::tree::tree_runtime::cli::run_tree_summarizer_command(&args[1..]),
        "memory" => crate::core::memory_cli::run_memory_command(&args[1..]),
        "agent" => crate::core::agent_cli::run_agent_command(&args[1..]),
        "sentry-test" => run_sentry_test_command(&args[1..]),
        // Generic namespace dispatcher
        namespace => run_namespace_command(namespace, &args[1..], &grouped),
    }
}

Top-level commands receive special handling because they require custom argument parsing or direct domain integration rather than generic JSON-RPC invocation.

The Generic Namespace Dispatch Mechanism

When the first argument does not match a built-in command, OpenHuman treats it as an RPC namespace. The run_namespace_command function looks up the requested namespace in the grouped controller schemas and executes the method via invoke_method.

The helper grouped_schemas() (defined in src/core/cli.rs) aggregates all registered schemas, indexes them by namespace, and makes them available to the dispatcher. When a user runs:

openhuman <namespace> <method> [...args]

The dispatcher performs the following:

  • Verifies that <namespace> exists in the grouped schemas.
  • Confirms that <method> is a valid entry in that namespace's method table.
  • Converts remaining CLI arguments to JSON using parse_json_params.
  • Invokes invoke_method to execute the RPC handler inside the core.

This design ensures that any RPC method automatically becomes a CLI subcommand without additional wiring in cli.rs.

Registering New Commands via Controller Schemas

OpenHuman does not maintain a hard-coded list of RPC methods in the CLI layer. Instead, each domain declares controller schemas that describe available namespaces and their functions.

Registration occurs in two locations:

A typical controller schema defines the namespace, method handlers, and metadata:

// src/openhuman/example/schemas.rs
use crate::core::{ControllerSchema, RpcContext, RpcOutcome};
use serde_json::{json, Value};

pub fn register_example_controller() -> Vec<ControllerSchema> {
    vec![ControllerSchema {
        namespace: "example",
        methods: &[("echo", handle_echo)],
        description: "Example echo commands",
    }]
}

fn handle_echo(_ctx: RpcContext, params: Value) -> RpcOutcome {
    let message = params.get("message")
        .and_then(Value::as_str)
        .unwrap_or("default");
    RpcOutcome::json(json!({ "echo": message }))
}

Adding this controller to src/core/all.rs immediately exposes openhuman example echo as a valid CLI invocation.

Step-by-Step: Adding a Custom CLI Command

To register a new command in the OpenHuman CLI, follow this implementation pattern:

1. Define the Controller Schema

Create or modify the domain's schemas.rs to include the new namespace and method handler:

// src/openhuman/greetings/schemas.rs
use crate::core::{ControllerSchema, RpcContext, RpcOutcome};
use serde_json::{json, Value};

pub fn register_greetings_controller() -> Vec<ControllerSchema> {
    vec![ControllerSchema {
        namespace: "greetings",
        methods: &[("greet", handle_greet)],
        description: "Simple greeting commands",
    }]
}

fn handle_greet(_ctx: RpcContext, params: Value) -> RpcOutcome {
    let name = params.get("name")
        .and_then(Value::as_str)
        .unwrap_or("world");
    RpcOutcome::json(json!({ "message": format!("Hello, {}!", name) }))
}

2. Add to the Global Registry

Include the registration function in src/core/all.rs:

mod openhuman::greetings;
register_controllers!(openhuman::greetings::register_greetings_controller);

3. Verify via CLI

The command becomes immediately available:

openhuman greetings greet --name Alice

# Output: {"message":"Hello, Alice!"}

4. Run Tests

The CLI test suite in src/core/cli_capability.rs verifies that every registered controller appears in the dispatch table, ensuring your new command is reachable.

Extending Top-Level Subcommands

For commands requiring custom argument parsing or specialized initialization (like memory or agent), you must add explicit routing in src/core/cli.rs rather than using the generic dispatcher.

Edit the match arm in run_from_cli_args to include your command, then implement a thin wrapper in a dedicated CLI module:

// src/core/memory_cli.rs
pub fn run_memory_command(args: &[String]) -> Result<()> {
    match args.get(0).map(|s| s.as_str()) {
        Some("status") => run_memory_status(&args[1..]),
        Some("clear") => run_memory_clear(&args[1..]),
        Some("custom") => run_memory_custom(&args[1..]), // Your new command
        other => Err(anyhow!("unknown memory subcommand `{:?}`", other)),
    }
}

Reference this wrapper in the main dispatch match using crate::core::memory_cli::run_memory_command.

Summary

  • Central dispatch in src/core/cli.rs routes arguments through run_from_cli_args, handling global flags, TUI initialization, and command matching.
  • Generic namespace dispatch (lines 1090-1112) treats unrecognized first arguments as RPC namespaces, looking up methods in grouped_schemas() and invoking them via invoke_method.
  • Controller schemas define CLI commands declaratively; domains implement schemas.rs files that map namespace strings to handler functions.
  • Global registration occurs in src/core/all.rs through the register_controllers! macro, automatically surfacing RPC methods as CLI subcommands.
  • Top-level exceptions like memory or agent require explicit match arms in cli.rs and dedicated CLI helper modules (src/core/memory_cli.rs, src/core/agent_cli.rs).
  • Testing coverage in src/core/cli_capability.rs validates that every registered controller is dispatchable from the command line.

Frequently Asked Questions

How does OpenHuman decide between top-level and namespace commands?

The dispatcher checks args[0] against a hard-coded list in run_from_cli_args. If the argument matches strings like run, serve, mcp, tui, call, tree-summarizer, memory, agent, or sentry-test, it routes to the specialized handler. Any other string falls through to run_namespace_command, which treats the value as an RPC namespace and attempts to resolve it against the grouped controller schemas.

What file do I edit to add a new RPC-based CLI command?

Create or modify the schemas.rs file in your domain directory (e.g., src/openhuman/yourdomain/schemas.rs) to define the ControllerSchema and handler function. Then register the controller in src/core/all.rs using register_controllers!. You do not need to modify src/core/cli.rs for standard RPC methods because the generic dispatcher handles them automatically.

How are CLI commands tested in OpenHuman?

The test suite in src/core/cli_capability.rs validates that every controller registered in the global schema appears as a dispatchable CLI command. This ensures that newly added namespaces and methods are reachable from the terminal without writing separate integration tests for each CLI subcommand.

Can I create a custom top-level subcommand like memory or agent?

Yes, but this requires explicit routing. Add a new match arm in run_from_cli_args within src/core/cli.rs that points to a dedicated CLI helper module (following the pattern of src/core/memory_cli.rs or src/core/agent_cli.rs). This approach is necessary when your command needs custom argument parsing, special initialization, or bypasses the standard JSON-RPC invocation flow.

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 →