refactor(core): move CLI adapters out of src/core/ into per-domain cli modules (#758)

* refactor(voice): move standalone CLI adapter into voice domain

Move the blocking `openhuman voice` / `openhuman dictate` dictation-server
subcommand out of `src/core/cli.rs` and into a new `src/openhuman/voice/cli.rs`
owned by the voice domain. The core CLI dispatcher now routes to
`voice::cli::run_standalone_subcommand` and no longer imports voice internals.

Why this shape: the standalone server blocks forever on the hotkey listener,
which doesn't fit the controller registry's request/response contract. A
domain-owned CLI adapter is the right home for long-lived operational
commands; the controller registry stays for RPC-style capabilities.

Establishes the exposure rule documented in PLAN.md for the rest of the
backend overhaul.

- src/openhuman/voice/cli.rs: new 118-line adapter (flag parsing, tokio
  runtime, run_standalone call)
- src/openhuman/voice/mod.rs: declare `pub mod cli`
- src/core/cli.rs: delete 92-line run_voice_server_command, dispatch only
- PLAN.md: new scope doc + exposure rule

* refactor(text_input): move CLI adapter into domain

Move src/core/text_input_cli.rs to src/openhuman/text_input/cli.rs and
drop the stale `pub mod text_input_cli` from src/core/mod.rs. Core CLI
dispatcher now routes `text-input` to the domain-owned adapter.

The moved file has a mixed shape that validates the exposure rule from
the previous voice commit:
 - `run` starts a long-lived HTTP JSON-RPC server (domain cli.rs shape)
 - `read / insert / ghost / dismiss` are short-lived UX wrappers around
   `text_input::rpc::*` (pretty-print, flag parsing — CLI affordance, not
   transport duplication)

Both shapes legitimately belong in the domain. The controller registry
stays for transport-agnostic capabilities; CLI UX wrappers live next to
their domain RPC.

- git mv src/core/text_input_cli.rs src/openhuman/text_input/cli.rs
- src/openhuman/text_input/mod.rs: declare `pub mod cli`
- src/core/mod.rs: drop `pub mod text_input_cli`
- src/core/cli.rs: update dispatch path

* refactor(tree_summarizer): move CLI adapter into domain

git mv src/core/tree_summarizer_cli.rs src/openhuman/tree_summarizer/cli.rs
and drop the stale `pub mod tree_summarizer_cli` from src/core/mod.rs.
Core CLI dispatcher now routes `tree-summarizer` to the domain-owned
adapter.

All five subcommands (ingest / run / query / status / rebuild) are short
-lived UX wrappers over registry handlers that already exist in
tree_summarizer/schemas.rs — no long-running server, no business logic
duplicated. Straight relocation keeps transport generic.

- git mv src/core/tree_summarizer_cli.rs src/openhuman/tree_summarizer/cli.rs
- src/openhuman/tree_summarizer/mod.rs: declare `pub mod cli`
- src/core/mod.rs: drop `pub mod tree_summarizer_cli`
- src/core/cli.rs: update dispatch path

* refactor(screen_intelligence): move + split CLI adapter into domain

Move src/core/screen_intelligence_cli.rs (699 lines, 7 subcommands) into
a domain-owned cli/ submodule, split by responsibility so no single file
exceeds the project's 500-line soft limit.

Layout:
  screen_intelligence/cli/mod.rs        (204) dispatch + shared helpers
                                              (CliOpts, parse_opts,
                                              bootstrap_engine, logging,
                                              print_help)
  screen_intelligence/cli/server.rs      (79) run_server (long-running
                                              capture+vision loop)
  screen_intelligence/cli/session.rs    (150) status / start / stop
  screen_intelligence/cli/capture.rs    (173) capture / vision (inspect pair)
  screen_intelligence/cli/doctor.rs     (107) readiness diagnostics

Classification before moving:
 - no business-logic duplication vs schemas.rs handlers
 - heavy fns (doctor 103, capture 97, start 89) were 80%+ pretty-printing
   and CLI-side orchestration (polling loop, flag-driven save) — legit
   CLI UX, not extracted domain logic
 - shared helpers stayed pub(super) inside mod.rs; no new domain APIs
   introduced

Core transport changes:
 - delete src/core/screen_intelligence_cli.rs
 - drop `pub mod screen_intelligence_cli` from src/core/mod.rs
 - src/core/cli.rs dispatch routes to domain cli::

After this commit, src/core/cli.rs contains only dispatch lines for all
four migrated domains (voice, text_input, tree_summarizer,
screen_intelligence) — no embedded domain logic.

* refactor(screen_intelligence): address external review — extract capture save + tighten cli visibility

Follow-ups from codex + gemini review of the branch:

 1. Extract CaptureFrame construction + disk save from the CLI.
    `capture.rs --keep` was building a CaptureFrame inline (stamping
    timestamps, copying context fields) and calling
    `AccessibilityEngine::save_screenshot_to_disk` directly. Added
    `AccessibilityEngine::save_capture_test_result(workspace_dir,
    &CaptureTestResult, reason) -> Option<Result<PathBuf, String>>`
    so the CLI only passes the result through and picks a reason
    label. Domain owns the frame shape, not the CLI.

 2. Tighten CLI module visibility across all four migrated domains.
    `pub mod cli` -> `pub(crate) mod cli` and
    `pub fn run_*_command` -> `pub(crate) fn` for voice, text_input,
    tree_summarizer, screen_intelligence. Only src/core/cli.rs needs
    to see these; they're transport plumbing, not domain public API.

No behavior change.

* style: cargo fmt

* address review comments from coderabbitai

- screen_intelligence/cli/server: use effective keep_screenshots (opts.keep
  || config flag) for both SiServerConfig and status output, so the server
  behaves consistently with what the CLI reports.
- voice/cli: surface config load errors with a warning instead of silently
  falling back to Config::default(); reject unknown --mode values instead
  of silently coercing them to Push.
- PLAN.md: narrow the Phase 4 enforcement grep so it forbids domain
  imports/non-dispatcher references in the transport layer while still
  allowing crate::openhuman::<domain>::cli::run_*(...) dispatch calls,
  which this PR legitimately introduces.

* style: cargo fmt voice cli

* style: cargo fmt + bump tauri-cef vendor

* fix(screen_intelligence): narrow save_capture_test_result to pub(crate) (addresses @coderabbitai nitpick on engine.rs:486)

AccessibilityEngine::save_capture_test_result is only called from the
domain's own cli/capture.rs (crate-internal). Making it pub leaks it
into the public API surface unnecessarily — tighten to pub(crate) to
match the visibility-tightening done across the rest of this PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* update

* chore: update .gitignore to include scheduled_tasks.lock in app/.claude

* ran formatter

---------

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jwalin Shah
2026-04-22 14:30:31 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6 Jwalin Shah Steven Enamakel
parent 8212438112
commit 325f019853
26 changed files with 1008 additions and 851 deletions
+4 -97
View File
@@ -61,12 +61,12 @@ pub fn run_from_cli_args(args: &[String]) -> Result<()> {
"call" => run_call_command(&args[1..]),
// Domain-specific CLI adapters that don't follow the generic namespace pattern.
"screen-intelligence" => {
crate::core::screen_intelligence_cli::run_screen_intelligence_command(&args[1..])
crate::openhuman::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..]),
"voice" | "dictate" => crate::openhuman::voice::cli::run_standalone_subcommand(&args[1..]),
"text-input" => crate::openhuman::text_input::cli::run_text_input_command(&args[1..]),
"tree-summarizer" => {
crate::core::tree_summarizer_cli::run_tree_summarizer_command(&args[1..])
crate::openhuman::tree_summarizer::cli::run_tree_summarizer_command(&args[1..])
}
"memory" => crate::core::memory_cli::run_memory_command(&args[1..]),
"agent" => {
@@ -334,99 +334,6 @@ fn run_call_command(args: &[String]) -> Result<()> {
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.