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:
- Global launch options –
parse_launch_optionsextracts flags such as--modelor--providerthat affect the entire process. - TUI auto-launch – If the user requests
tui/chator heuristics inshould_auto_launch_tuitrigger interactive mode,run_tui_from_clitakes over. - Help handling – When no arguments are supplied or the token is
--help,print_general_helpdisplays available domains. - Top-level subcommands – A
matchstatement onargs[0]handles built-in commands (run,serve,mcp,tui,call,tree-summarizer,memory,agent,sentry-test). - 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_methodto 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:
- Domain schemas – Each domain contains a
schemas.rsfile (e.g.,src/openhuman/agent/registry/schemas.rs,src/openhuman/memory/ops/schemas.rs) that definesControllerSchemastructs mapping method names to handlers. - Global aggregation –
src/core/all.rscollects all domain registrations via theregister_controllers!macro, creating the unified registry thatgrouped_schemas()consumes.
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.rsroutes arguments throughrun_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 viainvoke_method. - Controller schemas define CLI commands declaratively; domains implement
schemas.rsfiles that map namespace strings to handler functions. - Global registration occurs in
src/core/all.rsthrough theregister_controllers!macro, automatically surfacing RPC methods as CLI subcommands. - Top-level exceptions like
memoryoragentrequire explicit match arms incli.rsand dedicated CLI helper modules (src/core/memory_cli.rs,src/core/agent_cli.rs). - Testing coverage in
src/core/cli_capability.rsvalidates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →