mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
* refactor: remove unused socket, agent tool registry, daemon health, and API service files - Deleted the `useSocket` hook, `AgentToolRegistry`, `DaemonHealthService`, and various API service files including `actionableItemsApi`, `apiKeysApi`, `feedbackApi`, `inferenceApi`, `managedDmApi`, and `settingsApi`. - This cleanup reduces code complexity and improves maintainability by removing obsolete components that are no longer in use. * feat: add knip configuration and commands to package.json - Introduced new scripts for running knip in development and production modes in both package.json files. - Added a knip.json configuration file to specify entry points and project files for dependency analysis. - Updated yarn.lock to include new dependencies related to knip, enhancing the project's dependency management capabilities. This commit improves the project's tooling for managing dependencies and ensures better code quality through automated checks. * refactor: remove unused components and clean up dependencies - Deleted several unused components related to intelligence features, including ActionPanel, InputGroup, SectionCard, and ValidatedField, to streamline the codebase. - Removed mock data and country data files that are no longer in use, enhancing maintainability. - Cleaned up the package.json by removing the @heroicons/react dependency, which is no longer required. - This commit improves the overall project structure and reduces complexity by eliminating obsolete code. * refactor: remove IntelligenceApiService and redefine ConnectedTool interface - Deleted the `IntelligenceApiService` class and its associated backend API methods to streamline the codebase. - Introduced a local definition of the `ConnectedTool` interface in `useIntelligenceApiFallback.ts` for better encapsulation and clarity. - This refactor enhances maintainability by eliminating unused code and consolidating relevant types within the appropriate context. * style: format knip config * chore(dependencies): update OpenHuman to version 0.52.0 in Cargo.lock * feat: implement Daemon Health Service for polling and state management - Introduced the `DaemonHealthService` class to poll the Rust core health snapshot and synchronize the frontend daemon store. - Added methods for setting up a health listener, parsing health snapshots, and updating the daemon store based on health data. - Implemented a timeout mechanism to handle disconnection scenarios, enhancing the reliability of the daemon's health monitoring. - This addition improves the application's ability to maintain an accurate representation of the daemon's health status in real-time. * chore(knip): update entry points in knip configuration - Modified the `entry` field in `knip.json` to include `src/main.tsx` alongside existing test specifications. - This change ensures that the main application file is included in dependency analysis, improving project structure and tooling. * refactor: streamline code and enhance readability across multiple modules - Consolidated multiple `replace` calls into single calls using arrays for improved efficiency in text processing. - Simplified default implementations for several structs, removing redundant code. - Updated query mapping in database interactions to enhance clarity and maintainability. - Improved logging and error handling by refining how state and error messages are processed. - Enhanced the readability of various functions by restructuring conditional checks and simplifying logic. These changes collectively improve code maintainability and performance across the application. * Merge remote-tracking branch 'origin/fix/cleanup' into fix/cleanup * fix: update error handling in bootstrap_after_login function - Changed the error parameter in the inspect_err closure to an underscore to indicate it is unused. - This minor adjustment improves code clarity and adheres to Rust conventions for unused variables.
606 lines
22 KiB
Rust
606 lines
22 KiB
Rust
//! Command-line interface for the OpenHuman core binary.
|
|
//!
|
|
//! This module handles argument parsing, subcommand dispatching, and help printing
|
|
//! for the CLI. It supports commands for running the server, making RPC calls,
|
|
//! starting a REPL, and invoking domain-specific functionality across various namespaces.
|
|
|
|
use anyhow::Result;
|
|
use serde_json::{Map, Value};
|
|
use std::collections::BTreeMap;
|
|
|
|
use crate::core::all;
|
|
use crate::core::autocomplete_cli_adapter;
|
|
use crate::core::jsonrpc::{default_state, invoke_method, parse_json_params};
|
|
use crate::core::logging::CliLogDefault;
|
|
use crate::core::{ControllerSchema, TypeSchema};
|
|
|
|
/// The ASCII banner displayed when the CLI starts.
|
|
const CLI_BANNER: &str = r#"
|
|
|
|
▗▄▖ ▄▄▄▄ ▗▞▀▚▖▄▄▄▄ ▗▖ ▗▖█ ▐▌▄▄▄▄ ▗▞▀▜▌▄▄▄▄
|
|
▐▌ ▐▌█ █ ▐▛▀▀▘█ █ ▐▌ ▐▌▀▄▄▞▘█ █ █ ▝▚▄▟▌█ █
|
|
▐▌ ▐▌█▄▄▄▀ ▝▚▄▄▖█ █ ▐▛▀▜▌ █ █ █ █
|
|
▝▚▄▞▘█ ▐▌ ▐▌
|
|
▀
|
|
|
|
Contribute & Star us on GitHub: https://github.com/tinyhumansai/openhuman
|
|
|
|
"#;
|
|
|
|
/// Dispatches CLI commands based on arguments.
|
|
///
|
|
/// This is the entry point for CLI argument handling. It prints the banner,
|
|
/// checks for help requests, and dispatches to specific command handlers
|
|
/// like `run`, `call`, `repl`, `skills`, or namespace-based commands.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `args` - A slice of strings containing the command-line arguments (excluding the binary name).
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the command fails or if an unknown command is provided.
|
|
pub fn run_from_cli_args(args: &[String]) -> Result<()> {
|
|
// Print the welcome banner to stderr to keep stdout clean for JSON output.
|
|
eprint!("{CLI_BANNER}");
|
|
|
|
let grouped = grouped_schemas();
|
|
if args.is_empty() || is_help(&args[0]) {
|
|
print_general_help(&grouped);
|
|
return Ok(());
|
|
}
|
|
|
|
// Match on the first argument to determine the subcommand.
|
|
match args[0].as_str() {
|
|
"run" | "serve" => run_server_command(&args[1..]),
|
|
"call" => run_call_command(&args[1..]),
|
|
"repl" | "shell" => crate::core::repl::run_repl(&args[1..]),
|
|
"skills" => crate::core::skills_cli::run_skills_command(&args[1..]),
|
|
"screen-intelligence" => {
|
|
crate::core::screen_intelligence_cli::run_screen_intelligence_command(&args[1..])
|
|
}
|
|
"voice" | "dictate" => run_voice_server_command(&args[1..]),
|
|
"text-input" => crate::core::text_input_cli::run_text_input_command(&args[1..]),
|
|
"tree-summarizer" => {
|
|
crate::core::tree_summarizer_cli::run_tree_summarizer_command(&args[1..])
|
|
}
|
|
"memory" => crate::core::memory_cli::run_memory_command(&args[1..]),
|
|
namespace => run_namespace_command(namespace, &args[1..], &grouped),
|
|
}
|
|
}
|
|
|
|
/// Loads key/value pairs from a dotenv file into the process environment.
|
|
///
|
|
/// Precedence: variables already set in the environment are **not** overwritten.
|
|
/// Order: `OPENHUMAN_DOTENV_PATH` (if set to a non-empty path), else `.env` in the current working directory.
|
|
fn load_dotenv_for_server() -> Result<()> {
|
|
match std::env::var("OPENHUMAN_DOTENV_PATH") {
|
|
Ok(path) if !path.trim().is_empty() => {
|
|
dotenvy::from_path(&path).map_err(|e| {
|
|
anyhow::anyhow!("failed to load dotenv from OPENHUMAN_DOTENV_PATH={path}: {e}")
|
|
})?;
|
|
}
|
|
_ => {
|
|
let _ = dotenvy::dotenv();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Handles the `run` subcommand to start the core HTTP/JSON-RPC server.
|
|
///
|
|
/// Parses flags for port, host, and optional Socket.IO support.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `args` - Command-line arguments for the `run` command.
|
|
fn run_server_command(args: &[String]) -> Result<()> {
|
|
load_dotenv_for_server()?;
|
|
|
|
let mut port: Option<u16> = None;
|
|
let mut host: Option<String> = None;
|
|
let mut socketio_enabled = true;
|
|
let mut verbose = false;
|
|
let mut log_scope = CliLogDefault::Global;
|
|
let mut i = 0usize;
|
|
|
|
// Manual argument parsing loop for specific flags.
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--port" => {
|
|
let raw = args
|
|
.get(i + 1)
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for --port"))?;
|
|
port = Some(
|
|
raw.parse::<u16>()
|
|
.map_err(|e| anyhow::anyhow!("invalid --port: {e}"))?,
|
|
);
|
|
i += 2;
|
|
}
|
|
"--host" => {
|
|
host = Some(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for --host"))?
|
|
.clone(),
|
|
);
|
|
i += 2;
|
|
}
|
|
"--jsonrpc-only" => {
|
|
socketio_enabled = false;
|
|
i += 1;
|
|
}
|
|
"-v" | "--verbose" => {
|
|
verbose = true;
|
|
i += 1;
|
|
}
|
|
other if autocomplete_cli_adapter::parse_run_scope_flag(other).is_some() => {
|
|
log_scope = autocomplete_cli_adapter::parse_run_scope_flag(other)
|
|
.unwrap_or(CliLogDefault::Global);
|
|
i += 1;
|
|
}
|
|
"-h" | "--help" => {
|
|
println!("Usage: openhuman run [--host <addr>] [--port <u16>] [--jsonrpc-only] [--autocomplete-logs] [-v|--verbose]");
|
|
println!();
|
|
println!(
|
|
" --host <addr> Bind address (default: 127.0.0.1 or OPENHUMAN_CORE_HOST)"
|
|
);
|
|
println!(
|
|
" --port <u16> Listen address port (default: 7788 or OPENHUMAN_CORE_PORT)"
|
|
);
|
|
println!(" --jsonrpc-only HTTP JSON-RPC only; disable Socket.IO");
|
|
autocomplete_cli_adapter::print_run_scope_help_line();
|
|
println!(" -v, --verbose Shorthand for RUST_LOG=debug when RUST_LOG is unset");
|
|
println!();
|
|
println!("Logging: set RUST_LOG (e.g. RUST_LOG=debug openhuman run). Default level is info.");
|
|
return Ok(());
|
|
}
|
|
other => return Err(anyhow::anyhow!("unknown run arg: {other}")),
|
|
}
|
|
}
|
|
|
|
crate::core::logging::init_for_cli_run(verbose, log_scope);
|
|
|
|
// Initialize the Tokio runtime and start the server.
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
rt.block_on(async {
|
|
crate::core::jsonrpc::run_server(host.as_deref(), port, socketio_enabled).await
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Handles the `call` subcommand to invoke a JSON-RPC method directly from the CLI.
|
|
///
|
|
/// Useful for testing and automation.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `args` - Command-line arguments specifying the method and parameters.
|
|
fn run_call_command(args: &[String]) -> Result<()> {
|
|
let mut method: Option<String> = None;
|
|
let mut params = "{}".to_string();
|
|
|
|
let mut i = 0usize;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--method" => {
|
|
method = Some(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for --method"))?
|
|
.clone(),
|
|
);
|
|
i += 2;
|
|
}
|
|
"--params" => {
|
|
params = args
|
|
.get(i + 1)
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for --params"))?
|
|
.clone();
|
|
i += 2;
|
|
}
|
|
"-h" | "--help" => {
|
|
println!("Usage: openhuman call --method <name> [--params '<json>']");
|
|
return Ok(());
|
|
}
|
|
other => return Err(anyhow::anyhow!("unknown call arg: {other}")),
|
|
}
|
|
}
|
|
|
|
let method = method.ok_or_else(|| anyhow::anyhow!("--method is required"))?;
|
|
let params = parse_json_params(¶ms).map_err(anyhow::Error::msg)?;
|
|
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
let value = rt
|
|
.block_on(async { invoke_method(default_state(), &method, params).await })
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
// Output the result as pretty-printed JSON.
|
|
println!("{}", serde_json::to_string_pretty(&value)?);
|
|
Ok(())
|
|
}
|
|
|
|
/// Handles the `voice` subcommand to run the standalone voice dictation server.
|
|
///
|
|
/// Listens for a hotkey, records audio, transcribes via whisper, and inserts
|
|
/// the result into the active text field.
|
|
|
|
fn run_voice_server_command(args: &[String]) -> Result<()> {
|
|
use crate::openhuman::voice::hotkey::ActivationMode;
|
|
use crate::openhuman::voice::server::{run_standalone, VoiceServerConfig};
|
|
|
|
let mut hotkey: Option<String> = None;
|
|
let mut mode: Option<String> = None;
|
|
let mut skip_cleanup = false;
|
|
let mut verbose = false;
|
|
let mut i = 0usize;
|
|
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--hotkey" => {
|
|
hotkey = Some(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for --hotkey"))?
|
|
.clone(),
|
|
);
|
|
i += 2;
|
|
}
|
|
"--mode" => {
|
|
mode = Some(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for --mode"))?
|
|
.clone(),
|
|
);
|
|
i += 2;
|
|
}
|
|
"--skip-cleanup" => {
|
|
skip_cleanup = true;
|
|
i += 1;
|
|
}
|
|
"-v" | "--verbose" => {
|
|
verbose = true;
|
|
i += 1;
|
|
}
|
|
"-h" | "--help" => {
|
|
println!("Usage: openhuman voice [--hotkey <combo>] [--mode <tap|push>] [--skip-cleanup] [-v]");
|
|
println!();
|
|
println!(" --hotkey <combo> Key combination (default: fn)");
|
|
println!(
|
|
" --mode <tap|push> Activation: tap to toggle, push to hold (default: push)"
|
|
);
|
|
println!(" --skip-cleanup Skip LLM post-processing on transcriptions");
|
|
println!(" -v, --verbose Enable debug logging");
|
|
println!();
|
|
println!("Standalone voice dictation server. Press the hotkey to dictate,");
|
|
println!("transcribed text is inserted into the active text field.");
|
|
return Ok(());
|
|
}
|
|
other => return Err(anyhow::anyhow!("unknown voice arg: {other}")),
|
|
}
|
|
}
|
|
|
|
crate::core::logging::init_for_cli_run(verbose, CliLogDefault::Global);
|
|
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
|
|
rt.block_on(async {
|
|
let mut config = crate::openhuman::config::Config::load_or_init()
|
|
.await
|
|
.unwrap_or_default();
|
|
config.apply_env_overrides();
|
|
|
|
let activation_mode = match mode.as_deref() {
|
|
Some("tap") => ActivationMode::Tap,
|
|
_ => ActivationMode::Push,
|
|
};
|
|
|
|
let server_config = VoiceServerConfig {
|
|
hotkey: hotkey.unwrap_or_else(|| config.voice_server.hotkey.clone()),
|
|
activation_mode,
|
|
skip_cleanup,
|
|
context: None,
|
|
min_duration_secs: config.voice_server.min_duration_secs,
|
|
silence_threshold: config.voice_server.silence_threshold,
|
|
custom_dictionary: config.voice_server.custom_dictionary.clone(),
|
|
};
|
|
|
|
run_standalone(config, server_config)
|
|
.await
|
|
.map_err(anyhow::Error::msg)
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Dispatches commands that fall under a specific namespace (e.g., `openhuman <namespace> <function>`).
|
|
///
|
|
/// It looks up the function schema for validation and executes the request.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `namespace` - The namespace for the command.
|
|
/// * `args` - Arguments for the function within the namespace.
|
|
/// * `grouped` - A map of available schemas grouped by namespace.
|
|
fn run_namespace_command(
|
|
namespace: &str,
|
|
args: &[String],
|
|
grouped: &BTreeMap<String, Vec<ControllerSchema>>,
|
|
) -> Result<()> {
|
|
let Some(schemas) = grouped.get(namespace) else {
|
|
return Err(anyhow::anyhow!(
|
|
"unknown namespace '{namespace}'. Run `openhuman --help` to see available namespaces."
|
|
));
|
|
};
|
|
|
|
let preparsed = autocomplete_cli_adapter::preparse_namespace(namespace, args);
|
|
let args: &[String] = &preparsed.args;
|
|
if let Some((verbose, scope)) = preparsed.init_logging {
|
|
crate::core::logging::init_for_cli_run(verbose, scope);
|
|
}
|
|
|
|
if args.is_empty() || is_help(&args[0]) {
|
|
print_namespace_help(namespace, schemas);
|
|
return Ok(());
|
|
}
|
|
|
|
let function = args[0].as_str();
|
|
let Some(schema) = schemas.iter().find(|s| s.function == function).cloned() else {
|
|
return Err(anyhow::anyhow!(
|
|
"unknown function '{namespace} {function}'. Run `openhuman {namespace} --help`."
|
|
));
|
|
};
|
|
|
|
// Domain adapters can intercept specific namespace/function combinations.
|
|
if args.len() > 1
|
|
&& is_help(&args[1])
|
|
&& autocomplete_cli_adapter::maybe_print_start_help(namespace, function)
|
|
{
|
|
return Ok(());
|
|
}
|
|
if let Some(value) =
|
|
autocomplete_cli_adapter::maybe_handle_namespace_start(namespace, function, &args[1..])?
|
|
{
|
|
println!("{}", serde_json::to_string_pretty(&value)?);
|
|
return Ok(());
|
|
}
|
|
|
|
if args.len() > 1 && is_help(&args[1]) {
|
|
print_function_help(namespace, &schema);
|
|
return Ok(());
|
|
}
|
|
|
|
// Generic parameter parsing and validation based on schema.
|
|
let params = parse_function_params(&schema, &args[1..]).map_err(anyhow::Error::msg)?;
|
|
let method = all::rpc_method_from_parts(namespace, function)
|
|
.ok_or_else(|| anyhow::anyhow!("unregistered controller '{namespace}.{function}'"))?;
|
|
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
let value = rt
|
|
.block_on(async { invoke_method(default_state(), &method, Value::Object(params)).await })
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
println!("{}", serde_json::to_string_pretty(&value)?);
|
|
Ok(())
|
|
}
|
|
|
|
/// Parses command-line arguments into a JSON map based on a function's schema.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `schema` - The schema defining expected inputs.
|
|
/// * `args` - The command-line arguments to parse.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if arguments are malformed, unknown, or fail validation.
|
|
fn parse_function_params(
|
|
schema: &ControllerSchema,
|
|
args: &[String],
|
|
) -> Result<Map<String, Value>, String> {
|
|
let mut out = Map::new();
|
|
let mut i = 0usize;
|
|
|
|
while i < args.len() {
|
|
let raw = &args[i];
|
|
if !raw.starts_with("--") {
|
|
return Err(format!("invalid arg '{raw}', expected --<param> <value>"));
|
|
}
|
|
let key = raw.trim_start_matches("--").replace('-', "_");
|
|
let Some(spec) = schema.inputs.iter().find(|input| input.name == key) else {
|
|
return Err(format!(
|
|
"unknown param '{key}' for {}.{}",
|
|
schema.namespace, schema.function
|
|
));
|
|
};
|
|
let raw_value = args
|
|
.get(i + 1)
|
|
.ok_or_else(|| format!("missing value for --{key}"))?;
|
|
let value = parse_input_value(&spec.ty, raw_value)?;
|
|
out.insert(key, value);
|
|
i += 2;
|
|
}
|
|
|
|
all::validate_params(schema, &out)?;
|
|
Ok(out)
|
|
}
|
|
|
|
/// Re-exported alias for parsing input values, used by the REPL.
|
|
pub fn parse_input_value_for_repl(ty: &TypeSchema, raw: &str) -> Result<Value, String> {
|
|
parse_input_value(ty, raw)
|
|
}
|
|
|
|
/// Parses a raw string value into a JSON `Value` based on the target `TypeSchema`.
|
|
///
|
|
/// Supports basic types like string, bool, and numbers, as well as complex JSON
|
|
/// structures for advanced types.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `ty` - The expected type schema.
|
|
/// * `raw` - The raw string value from the command line.
|
|
fn parse_input_value(ty: &TypeSchema, raw: &str) -> Result<Value, String> {
|
|
match ty {
|
|
TypeSchema::String => Ok(Value::String(raw.to_string())),
|
|
TypeSchema::Bool => raw
|
|
.parse::<bool>()
|
|
.map(Value::Bool)
|
|
.map_err(|e| format!("expected bool, got '{raw}': {e}")),
|
|
TypeSchema::I64 => raw
|
|
.parse::<i64>()
|
|
.map(|n| Value::Number(n.into()))
|
|
.map_err(|e| format!("expected i64, got '{raw}': {e}")),
|
|
TypeSchema::U64 => raw
|
|
.parse::<u64>()
|
|
.map(|n| Value::Number(n.into()))
|
|
.map_err(|e| format!("expected u64, got '{raw}': {e}")),
|
|
TypeSchema::F64 => {
|
|
let n = raw
|
|
.parse::<f64>()
|
|
.map_err(|e| format!("expected f64, got '{raw}': {e}"))?;
|
|
serde_json::Number::from_f64(n)
|
|
.map(Value::Number)
|
|
.ok_or_else(|| format!("invalid f64 '{raw}'"))
|
|
}
|
|
TypeSchema::Option(inner) => parse_input_value(inner, raw),
|
|
TypeSchema::Enum { .. } => Ok(Value::String(raw.to_string())),
|
|
TypeSchema::Json
|
|
| TypeSchema::Array(_)
|
|
| TypeSchema::Map(_)
|
|
| TypeSchema::Object { .. }
|
|
| TypeSchema::Ref(_)
|
|
| TypeSchema::Bytes => parse_json_params(raw),
|
|
}
|
|
}
|
|
|
|
/// Aggregates all registered controller schemas and groups them by namespace.
|
|
fn grouped_schemas() -> BTreeMap<String, Vec<ControllerSchema>> {
|
|
let mut grouped: BTreeMap<String, Vec<ControllerSchema>> = BTreeMap::new();
|
|
for schema in all::all_controller_schemas() {
|
|
grouped
|
|
.entry(schema.namespace.to_string())
|
|
.or_default()
|
|
.push(schema);
|
|
}
|
|
// Sort functions within each namespace for consistent help output.
|
|
for schemas in grouped.values_mut() {
|
|
schemas.sort_by_key(|s| s.function);
|
|
}
|
|
grouped
|
|
}
|
|
|
|
/// Prints the general help message listing available commands and namespaces.
|
|
fn print_general_help(grouped: &BTreeMap<String, Vec<ControllerSchema>>) {
|
|
println!("OpenHuman core CLI\n");
|
|
println!("Usage:");
|
|
println!(" openhuman run [--host <addr>] [--port <u16>] [--jsonrpc-only] [--verbose]");
|
|
println!(" openhuman repl [--verbose] [--eval '<cmd>'] [--batch]");
|
|
println!(" openhuman call --method <name> [--params '<json>']");
|
|
println!(" openhuman skills <subcommand> [options] (skill development runtime)");
|
|
println!(" openhuman voice [--hotkey <combo>] [--mode <tap|push>] (voice dictation server)");
|
|
println!(" openhuman tree-summarizer <subcommand> [options] (summary tree CLI)");
|
|
println!(" openhuman <namespace> <function> [--param value ...]\n");
|
|
println!("Available namespaces:");
|
|
for namespace in grouped.keys() {
|
|
let description = all::namespace_description(namespace.as_str())
|
|
.unwrap_or("No namespace description available.");
|
|
println!(" {namespace} - {description}");
|
|
}
|
|
println!("\nUse `openhuman <namespace> --help` to see functions.");
|
|
}
|
|
|
|
/// Prints help for a specific namespace, listing its functions.
|
|
fn print_namespace_help(namespace: &str, schemas: &[ControllerSchema]) {
|
|
println!("Namespace: {namespace}\n");
|
|
if let Some(description) = all::namespace_description(namespace) {
|
|
println!("{description}\n");
|
|
}
|
|
println!("Functions:");
|
|
for schema in schemas {
|
|
println!(" {} - {}", schema.function, schema.description);
|
|
}
|
|
println!("\nUse `openhuman {namespace} <function> --help` for parameters.");
|
|
autocomplete_cli_adapter::maybe_print_namespace_help_footer(namespace);
|
|
}
|
|
|
|
/// Prints detailed help for a specific function, including its parameters and description.
|
|
fn print_function_help(namespace: &str, schema: &ControllerSchema) {
|
|
println!("{} {}\n", namespace, schema.function);
|
|
println!("{}", schema.description);
|
|
println!("\nParameters:");
|
|
if schema.inputs.is_empty() {
|
|
println!(" none");
|
|
} else {
|
|
for input in &schema.inputs {
|
|
let required = if input.required {
|
|
"required"
|
|
} else {
|
|
"optional"
|
|
};
|
|
println!(" --{} ({}) - {}", input.name, required, input.comment);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checks if a string represents a help flag.
|
|
fn is_help(value: &str) -> bool {
|
|
matches!(value, "-h" | "--help" | "help")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{grouped_schemas, parse_function_params, parse_input_value};
|
|
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
|
|
|
|
#[test]
|
|
fn grouped_schemas_contains_migrated_namespaces() {
|
|
let grouped = grouped_schemas();
|
|
assert!(grouped.contains_key("health"));
|
|
assert!(grouped.contains_key("doctor"));
|
|
assert!(grouped.contains_key("encrypt"));
|
|
assert!(grouped.contains_key("decrypt"));
|
|
assert!(grouped.contains_key("autocomplete"));
|
|
assert!(grouped.contains_key("config"));
|
|
assert!(grouped.contains_key("auth"));
|
|
assert!(grouped.contains_key("service"));
|
|
assert!(grouped.contains_key("migrate"));
|
|
assert!(grouped.contains_key("local_ai"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_function_params_rejects_unknown_param() {
|
|
let schema = ControllerSchema {
|
|
namespace: "test",
|
|
function: "echo",
|
|
description: "test schema",
|
|
inputs: vec![FieldSchema {
|
|
name: "message",
|
|
ty: TypeSchema::String,
|
|
required: true,
|
|
comment: "message text",
|
|
}],
|
|
outputs: vec![FieldSchema {
|
|
name: "result",
|
|
ty: TypeSchema::String,
|
|
required: true,
|
|
comment: "echo response",
|
|
}],
|
|
};
|
|
let args = vec!["--unknown".to_string(), "value".to_string()];
|
|
let err = parse_function_params(&schema, &args).expect_err("unknown param should fail");
|
|
assert!(err.contains("unknown param"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_input_value_rejects_invalid_bool() {
|
|
let err = parse_input_value(&TypeSchema::Bool, "not-a-bool")
|
|
.expect_err("invalid bool should fail");
|
|
assert!(err.contains("expected bool"));
|
|
}
|
|
}
|