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.
-3
View File
@@ -16,11 +16,8 @@ pub mod jsonrpc;
pub mod logging;
pub mod memory_cli;
pub mod rpc_log;
pub mod screen_intelligence_cli;
pub mod shutdown;
pub mod socketio;
pub mod text_input_cli;
pub mod tree_summarizer_cli;
pub mod types;
/// Canonical function contract for domain controllers.
-699
View File
@@ -1,699 +0,0 @@
//! `openhuman screen-intelligence` — standalone CLI for the screen intelligence loop.
//!
//! Boots **only** the screen intelligence engine (accessibility capture + local-AI
//! vision) without the full desktop app, Socket.IO, or skills runtime. Useful for
//! testing the capture → save → vision-analysis pipeline from a terminal.
//!
//! Usage:
//! openhuman screen-intelligence run [--ttl <secs>] [--keep] [-v]
//! openhuman screen-intelligence status [-v]
//! openhuman screen-intelligence capture [--keep] [-v]
//! openhuman screen-intelligence start [--ttl <secs>] [-v]
//! openhuman screen-intelligence stop [-v]
//! openhuman screen-intelligence doctor [-v]
//! openhuman screen-intelligence vision [--limit <n>] [-v]
use anyhow::Result;
use std::sync::Arc;
/// Entry point for `openhuman screen-intelligence <subcommand>`.
pub fn run_screen_intelligence_command(args: &[String]) -> Result<()> {
if args.is_empty() || is_help(&args[0]) {
print_help();
return Ok(());
}
match args[0].as_str() {
"run" => run_server(&args[1..]),
"status" => run_status(&args[1..]),
"capture" => run_capture(&args[1..]),
"start" => run_start_session(&args[1..]),
"stop" => run_stop_session(&args[1..]),
"doctor" => run_doctor(&args[1..]),
"vision" => run_vision(&args[1..]),
other => Err(anyhow::anyhow!(
"unknown screen-intelligence subcommand '{other}'. Run `openhuman screen-intelligence --help`."
)),
}
}
// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------
struct CliOpts {
verbose: bool,
ttl_secs: u64,
keep: bool,
limit: usize,
no_vision_model: bool,
}
fn parse_opts(args: &[String]) -> Result<(CliOpts, Vec<String>)> {
let mut verbose = false;
let mut ttl_secs: u64 = 300;
let mut keep = false;
let mut limit: usize = 10;
let mut no_vision_model = false;
let mut rest = Vec::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--no-vision-model" | "--ocr-only" => {
no_vision_model = true;
i += 1;
}
"--ttl" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --ttl"))?;
ttl_secs = val
.parse()
.map_err(|e| anyhow::anyhow!("invalid --ttl: {e}"))?;
i += 2;
}
"--limit" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --limit"))?;
limit = val
.parse()
.map_err(|e| anyhow::anyhow!("invalid --limit: {e}"))?;
i += 2;
}
"--keep" => {
keep = true;
i += 1;
}
"-v" | "--verbose" => {
verbose = true;
i += 1;
}
"-h" | "--help" => {
rest.push(args[i].clone());
i += 1;
}
_ => {
rest.push(args[i].clone());
i += 1;
}
}
}
Ok((
CliOpts {
verbose,
ttl_secs,
keep,
limit,
no_vision_model,
},
rest,
))
}
/// Bootstrap the screen intelligence engine with config.
async fn bootstrap_engine(
verbose: bool,
) -> Result<Arc<crate::openhuman::screen_intelligence::AccessibilityEngine>> {
bootstrap_engine_with_opts(verbose, false).await
}
/// Bootstrap with CLI overrides.
async fn bootstrap_engine_with_opts(
verbose: bool,
no_vision_model: bool,
) -> Result<Arc<crate::openhuman::screen_intelligence::AccessibilityEngine>> {
use crate::openhuman::config::Config;
use crate::openhuman::screen_intelligence::global_engine;
let mut config = Config::load_or_init()
.await
.map_err(|e| anyhow::anyhow!("config load failed: {e}"))?;
if no_vision_model {
config.screen_intelligence.use_vision_model = false;
}
let engine = global_engine();
let _ = engine
.apply_config(config.screen_intelligence.clone())
.await;
if verbose {
log::info!(
"[screen-intelligence-cli] engine initialized, enabled={}, vision={}, use_vision_model={}, keep_screenshots={}, workspace={}",
config.screen_intelligence.enabled,
config.screen_intelligence.vision_enabled,
config.screen_intelligence.use_vision_model,
config.screen_intelligence.keep_screenshots,
config.workspace_dir.display(),
);
}
Ok(engine)
}
// ---------------------------------------------------------------------------
// Subcommands
// ---------------------------------------------------------------------------
/// `openhuman screen-intelligence run` — start the standalone capture + vision loop.
///
/// Delegates to [`crate::openhuman::screen_intelligence::server::run_standalone`],
/// which boots the engine, starts a capture session, and blocks in a
/// monitoring loop logging captures and vision summaries until TTL or Ctrl+C.
fn run_server(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) {
println!("Usage: openhuman screen-intelligence run [--ttl <secs>] [--keep] [--no-vision-model] [-v]");
println!();
println!("Start the screen intelligence capture + vision loop.");
println!("Captures screenshots at baseline FPS, runs OCR and vision analysis,");
println!("and logs summaries. Blocks until TTL expires or Ctrl+C.");
println!();
println!(" --ttl <secs> Session duration (default: 300)");
println!(" --keep Keep screenshots on disk after vision processing");
println!(" --no-vision-model Skip the vision LLM — use OCR + text LLM only");
println!(" --ocr-only Alias for --no-vision-model");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::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
.map_err(|e| anyhow::anyhow!("config load failed: {e}"))?;
if opts.no_vision_model {
config.screen_intelligence.use_vision_model = false;
}
let server_config = crate::openhuman::screen_intelligence::server::SiServerConfig {
ttl_secs: opts.ttl_secs,
log_interval_secs: 5,
keep_screenshots: opts.keep,
};
let mode_label = if config.screen_intelligence.use_vision_model {
format!("vision LLM ({})", config.local_ai.vision_model_id)
} else {
"OCR + text LLM (no vision model)".to_string()
};
eprintln!();
eprintln!(" Screen Intelligence");
eprintln!(" ───────────────────");
eprintln!(" TTL: {}s", opts.ttl_secs);
eprintln!(" Mode: {}", mode_label);
eprintln!(
" FPS: {}",
config.screen_intelligence.baseline_fps
);
eprintln!(
" Keep screenshots: {}",
opts.keep || config.screen_intelligence.keep_screenshots
);
eprintln!();
eprintln!(" Capturing → Vision → Log. Press Ctrl+C to stop.");
eprintln!();
crate::openhuman::screen_intelligence::server::run_standalone(config, server_config)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
})
}
/// `openhuman screen-intelligence status` — print current engine status as JSON.
fn run_status(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman screen-intelligence status [-v]");
println!();
println!("Print current screen intelligence engine status (permissions, session, config).");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let engine = bootstrap_engine(opts.verbose).await?;
let status = engine.status().await;
println!(
"{}",
serde_json::to_string_pretty(&status).unwrap_or_else(|_| format!("{:?}", status))
);
Ok(())
})
}
/// `openhuman screen-intelligence capture` — take a single screenshot and print info.
fn run_capture(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman screen-intelligence capture [--keep] [-v]");
println!();
println!("Take a single screenshot, optionally save to workspace, and print diagnostics.");
println!();
println!(" --keep Save the screenshot to {{workspace}}/screenshots/");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let engine = bootstrap_engine(opts.verbose).await?;
let result = engine.capture_test().await;
if result.ok {
eprintln!(" Capture: OK");
eprintln!(" Mode: {}", result.capture_mode);
eprintln!(" Timing: {}ms", result.timing_ms);
if let Some(bytes) = result.bytes_estimate {
eprintln!(" Size: {} bytes", bytes);
}
if let Some(ctx) = &result.context {
eprintln!(
" App: {}",
ctx.app_name.as_deref().unwrap_or("unknown")
);
eprintln!(
" Window: {}",
ctx.window_title.as_deref().unwrap_or("unknown")
);
}
// Save to disk if --keep
if opts.keep {
if let Some(image_ref) = &result.image_ref {
let config = crate::openhuman::config::Config::load_or_init()
.await
.map_err(|e| anyhow::anyhow!("config load failed: {e}"))?;
let frame = crate::openhuman::screen_intelligence::CaptureFrame {
captured_at_ms: chrono::Utc::now().timestamp_millis(),
reason: "cli_capture".to_string(),
app_name: result
.context
.as_ref()
.and_then(|c| c.app_name.clone()),
window_title: result
.context
.as_ref()
.and_then(|c| c.window_title.clone()),
image_ref: Some(image_ref.clone()),
};
match crate::openhuman::screen_intelligence::AccessibilityEngine::save_screenshot_to_disk(
&config.workspace_dir,
&frame,
) {
Ok(path) => {
eprintln!(" Saved: {}", path.display());
}
Err(e) => {
eprintln!(" Save failed: {e}");
}
}
}
}
} else {
eprintln!(" Capture: FAILED");
if let Some(err) = &result.error {
eprintln!(" Error: {err}");
}
std::process::exit(1);
}
// Also print as JSON for machine-readable output.
let mut json_result = serde_json::to_value(&result).unwrap_or_default();
// Strip image_ref from JSON output (too large for terminal).
if let Some(obj) = json_result.as_object_mut() {
obj.remove("image_ref");
}
println!(
"{}",
serde_json::to_string_pretty(&json_result).unwrap_or_default()
);
Ok(())
})
}
/// `openhuman screen-intelligence start` — start a capture + vision session.
fn run_start_session(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!(
"Usage: openhuman screen-intelligence start [--ttl <secs>] [--no-vision-model] [-v]"
);
println!();
println!("Start a screen intelligence capture session with vision analysis.");
println!("The session runs until TTL expires or Ctrl+C is pressed.");
println!();
println!(" --ttl <secs> Session duration (default: 300, max: 3600)");
println!(" --no-vision-model Skip the vision LLM — use OCR + text LLM only");
println!(" --ocr-only Alias for --no-vision-model");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let engine = bootstrap_engine_with_opts(opts.verbose, opts.no_vision_model).await?;
let params = crate::openhuman::screen_intelligence::StartSessionParams {
consent: true,
ttl_secs: Some(opts.ttl_secs),
screen_monitoring: Some(true),
};
match engine.start_session(params).await {
Ok(session) => {
eprintln!(" Session started!");
eprintln!(" TTL: {}s", session.ttl_secs);
eprintln!(" Vision: {}", session.vision_enabled);
eprintln!(" Panic hotkey: {}", session.panic_hotkey);
eprintln!();
eprintln!(" Capturing screenshots and running vision analysis...");
eprintln!(" Press Ctrl+C to stop.");
eprintln!();
// Print periodic status updates until the session ends.
let mut tick = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
tick.tick().await;
let status = engine.status().await;
if !status.session.active {
eprintln!(
"\n Session ended: {}",
status
.session
.stop_reason
.unwrap_or_else(|| "unknown".into())
);
break;
}
eprintln!(
" [{}] captures={} vision={} queue={} last_app={:?}",
chrono::Utc::now().format("%H:%M:%S"),
status.session.capture_count,
status.session.vision_state,
status.session.vision_queue_depth,
status.session.last_context.as_deref().unwrap_or("-"),
);
if let Some(summary) = &status.session.last_vision_summary {
let truncated = if summary.chars().count() > 100 {
format!("{}", summary.chars().take(100).collect::<String>())
} else {
summary.clone()
};
eprintln!(" notes: {truncated}");
}
}
}
Err(e) => {
eprintln!(" Failed to start session: {e}");
std::process::exit(1);
}
}
Ok(())
})
}
/// `openhuman screen-intelligence stop` — stop an active session.
fn run_stop_session(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman screen-intelligence stop [-v]");
println!();
println!("Stop the active screen intelligence session.");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let engine = bootstrap_engine(opts.verbose).await?;
let session = engine.stop_session(Some("cli_stop".to_string())).await;
eprintln!(
" Session stopped: {}",
session
.stop_reason
.unwrap_or_else(|| "no active session".into())
);
Ok(())
})
}
/// `openhuman screen-intelligence doctor` — diagnostic readiness check.
fn run_doctor(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman screen-intelligence doctor [-v]");
println!();
println!("Check system readiness: permissions, platform support, vision config.");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let _engine = bootstrap_engine(opts.verbose).await?;
let doctor_json =
crate::openhuman::screen_intelligence::rpc::accessibility_doctor_cli_json()
.await
.map_err(|e| anyhow::anyhow!("doctor check failed: {e}"))?;
let summary = &doctor_json["result"]["summary"];
let recommendations = &doctor_json["result"]["recommendations"];
let permissions = &doctor_json["result"]["permissions"];
eprintln!(" Screen Intelligence Doctor");
eprintln!(" ──────────────────────────");
eprintln!();
let check = |ok: bool| if ok { "" } else { "" };
let platform_ok = summary["platform_supported"].as_bool().unwrap_or(false);
let screen_ok = summary["screen_capture_ready"].as_bool().unwrap_or(false);
let control_ok = summary["accessibility_ready"].as_bool().unwrap_or(false);
let input_ok = summary["input_monitoring_ready"].as_bool().unwrap_or(false);
let overall_ok = summary["overall_ready"].as_bool().unwrap_or(false);
eprintln!(" {} Platform supported", check(platform_ok));
eprintln!(" {} Screen recording", check(screen_ok));
eprintln!(" {} Accessibility automation", check(control_ok));
eprintln!(" {} Input monitoring", check(input_ok));
eprintln!();
// Vision config check.
let config = crate::openhuman::config::Config::load_or_init().await.ok();
if let Some(ref cfg) = config {
let si = &cfg.screen_intelligence;
let la = &cfg.local_ai;
eprintln!(" Config:");
eprintln!(" enabled: {}", si.enabled);
eprintln!(" vision_enabled: {}", si.vision_enabled);
eprintln!(" use_vision_model: {}", si.use_vision_model);
eprintln!(" baseline_fps: {}", si.baseline_fps);
eprintln!(" keep_screenshots: {}", si.keep_screenshots);
eprintln!(" local_ai.enabled: {}", la.enabled);
eprintln!(" local_ai.provider: {}", la.provider);
if si.vision_enabled && !la.enabled {
eprintln!(" ⚠ Vision is enabled but local_ai.enabled=false — vision analysis will fail");
}
}
eprintln!();
if overall_ok {
eprintln!(" ✓ Overall: READY");
} else {
eprintln!(" ✗ Overall: NOT READY");
eprintln!();
eprintln!(" Recommendations:");
if let Some(recs) = recommendations.as_array() {
for rec in recs {
if let Some(s) = rec.as_str() {
eprintln!("{s}");
}
}
}
}
eprintln!();
// Machine-readable JSON output.
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"summary": summary,
"permissions": permissions,
"config": config.as_ref().map(|c| serde_json::json!({
"enabled": c.screen_intelligence.enabled,
"vision_enabled": c.screen_intelligence.vision_enabled,
"use_vision_model": c.screen_intelligence.use_vision_model,
"baseline_fps": c.screen_intelligence.baseline_fps,
"keep_screenshots": c.screen_intelligence.keep_screenshots,
"local_ai_enabled": c.local_ai.enabled,
"local_ai_provider": c.local_ai.provider,
})),
}))
.unwrap_or_default()
);
Ok(())
})
}
/// `openhuman screen-intelligence vision` — inspect recent vision summaries.
fn run_vision(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman screen-intelligence vision [--limit <n>] [-v]");
println!();
println!("Print recent vision summaries from the active session.");
println!();
println!(" --limit <n> Maximum summaries to show (default: 10)");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let engine = bootstrap_engine(opts.verbose).await?;
let result = engine.vision_recent(Some(opts.limit)).await;
if result.summaries.is_empty() {
eprintln!(" No vision summaries available.");
eprintln!(" Start a session first: openhuman screen-intelligence start");
} else {
eprintln!(" {} vision summary(ies):\n", result.summaries.len());
for (i, s) in result.summaries.iter().enumerate() {
let ts = chrono::DateTime::from_timestamp_millis(s.captured_at_ms)
.map(|dt| dt.format("%H:%M:%S").to_string())
.unwrap_or_else(|| "?".to_string());
eprintln!(
" [{}] {}{} (confidence: {:.0}%)",
i + 1,
ts,
s.app_name.as_deref().unwrap_or("unknown"),
s.confidence * 100.0,
);
if !s.ui_state.is_empty() {
let truncated = if s.ui_state.chars().count() > 120 {
format!("{}", s.ui_state.chars().take(120).collect::<String>())
} else {
s.ui_state.clone()
};
eprintln!(" ui: {truncated}");
}
if !s.actionable_notes.is_empty() {
let truncated = if s.actionable_notes.chars().count() > 120 {
format!(
"{}",
s.actionable_notes.chars().take(120).collect::<String>()
)
} else {
s.actionable_notes.clone()
};
eprintln!(" notes: {truncated}");
}
eprintln!();
}
}
// Machine-readable output.
println!(
"{}",
serde_json::to_string_pretty(&result).unwrap_or_default()
);
Ok(())
})
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Quiet logging: only `warn` unless verbose (used for non-server subcommands).
fn init_quiet_logging(verbose: bool) {
if !verbose && std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "warn");
}
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
}
fn is_help(value: &str) -> bool {
matches!(value, "-h" | "--help" | "help")
}
fn print_help() {
println!("openhuman screen-intelligence — standalone screen intelligence runtime\n");
println!("Boots only the screen intelligence engine (accessibility capture + local-AI");
println!("vision) without the full desktop app, Socket.IO, or skills runtime.\n");
println!("Usage:");
println!(" openhuman screen-intelligence run [--ttl <secs>] [--no-vision-model] [-v]");
println!(" openhuman screen-intelligence status [-v]");
println!(" openhuman screen-intelligence capture [--keep] [-v]");
println!(" openhuman screen-intelligence start [--ttl <secs>] [--no-vision-model] [-v]");
println!(" openhuman screen-intelligence stop [-v]");
println!(" openhuman screen-intelligence doctor [-v]");
println!(" openhuman screen-intelligence vision [--limit <n>] [-v]");
println!();
println!("Subcommands:");
println!(" run Start the capture → vision → log loop (blocks until TTL/Ctrl+C)");
println!(" status Print current engine status (permissions, session, config)");
println!(" capture Take a single screenshot and print diagnostics");
println!(" start Start a capture + vision session (runs until TTL or Ctrl+C)");
println!(" stop Stop the active session");
println!(" doctor Check system readiness (permissions, vision config, platform)");
println!(" vision Print recent vision summaries from the active session");
println!();
println!("Common options:");
println!(" --ttl <secs> Session TTL (default: 300)");
println!(" --limit <n> Max vision summaries for 'vision' (default: 10)");
println!(" --keep Save screenshot to disk (for 'capture')");
println!(" --no-vision-model Skip vision LLM — use OCR + text LLM only");
println!(" --ocr-only Alias for --no-vision-model");
println!(" -v, --verbose Enable debug logging");
}
-388
View File
@@ -1,388 +0,0 @@
//! `openhuman text-input` — standalone CLI for text input intelligence.
//!
//! Reads, inserts, and previews text in the OS-focused input field without
//! starting the full desktop app. Useful for testing autocomplete, voice
//! input, and accessibility integration from a terminal.
//!
//! Usage:
//! openhuman text-input run [--port <u16>] [-v]
//! openhuman text-input read [-v] [--bounds]
//! openhuman text-input insert <text> [-v]
//! openhuman text-input ghost <text> [--ttl <ms>] [-v]
//! openhuman text-input dismiss [-v]
use anyhow::Result;
/// Entry point for `openhuman text-input <subcommand>`.
pub fn run_text_input_command(args: &[String]) -> Result<()> {
if args.is_empty() || is_help(&args[0]) {
print_help();
return Ok(());
}
match args[0].as_str() {
"run" => run_server(&args[1..]),
"read" => run_read(&args[1..]),
"insert" => run_insert(&args[1..]),
"ghost" => run_ghost(&args[1..]),
"dismiss" => run_dismiss(&args[1..]),
other => Err(anyhow::anyhow!(
"unknown text-input subcommand '{other}'. Run `openhuman text-input --help`."
)),
}
}
// ---------------------------------------------------------------------------
// Option parsing
// ---------------------------------------------------------------------------
struct CliOpts {
port: u16,
verbose: bool,
ttl_ms: u32,
include_bounds: bool,
}
fn parse_opts(args: &[String]) -> Result<(CliOpts, Vec<String>)> {
let mut port: u16 = 7798;
let mut verbose = false;
let mut ttl_ms: u32 = 3000;
let mut include_bounds = false;
let mut rest = Vec::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--port" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --port"))?;
port = val
.parse()
.map_err(|e| anyhow::anyhow!("invalid --port: {e}"))?;
i += 2;
}
"--ttl" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --ttl"))?;
ttl_ms = val
.parse()
.map_err(|e| anyhow::anyhow!("invalid --ttl: {e}"))?;
i += 2;
}
"--bounds" => {
include_bounds = true;
i += 1;
}
"-v" | "--verbose" => {
verbose = true;
i += 1;
}
"-h" | "--help" => {
rest.push(args[i].clone());
i += 1;
}
_ => {
rest.push(args[i].clone());
i += 1;
}
}
}
Ok((
CliOpts {
port,
verbose,
ttl_ms,
include_bounds,
},
rest,
))
}
// ---------------------------------------------------------------------------
// Subcommands
// ---------------------------------------------------------------------------
/// `openhuman text-input run` — start a minimal JSON-RPC server.
fn run_server(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) {
println!("Usage: openhuman text-input run [--port <u16>] [-v]");
println!();
println!("Start a lightweight JSON-RPC server exposing text input RPC methods.");
println!();
println!(" --port <u16> Listen port (default: 7798)");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
crate::core::logging::init_for_cli_run(
opts.verbose,
crate::core::logging::CliLogDefault::Global,
);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let app = build_router();
let bind_addr = format!("127.0.0.1:{}", opts.port);
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
log::info!("[text-input-cli] ready — http://{bind_addr}/rpc (JSON-RPC 2.0)");
eprintln!();
eprintln!(" Text input dev server listening on http://{bind_addr}");
eprintln!(" JSON-RPC endpoint: POST http://{bind_addr}/rpc");
eprintln!(" Health check: GET http://{bind_addr}/health");
eprintln!(" Press Ctrl+C to stop.");
eprintln!();
axum::serve(listener, app).await?;
Ok(())
})
}
/// `openhuman text-input read` — one-shot read of the focused field.
fn run_read(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman text-input read [--bounds] [-v]");
println!();
println!("Read the currently focused text input field and print JSON to stdout.");
println!();
println!(" --bounds Include element bounds in the output");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let params = crate::openhuman::text_input::ReadFieldParams {
include_bounds: Some(opts.include_bounds),
};
let outcome = crate::openhuman::text_input::rpc::read_field(params)
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!(
"{}",
serde_json::to_string_pretty(&outcome.value)
.unwrap_or_else(|_| format!("{:?}", outcome.value))
);
Ok(())
})
}
/// `openhuman text-input insert <text>` — insert text into the focused field.
fn run_insert(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!("Usage: openhuman text-input insert <text> [-v]");
println!();
println!("Insert text into the currently focused input field.");
println!();
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
init_quiet_logging(opts.verbose);
let text = rest.join(" ");
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let params = crate::openhuman::text_input::InsertTextParams {
text,
validate_focus: None,
expected_app: None,
expected_role: None,
};
let outcome = crate::openhuman::text_input::rpc::insert_text(params)
.await
.map_err(|e| anyhow::anyhow!(e))?;
if outcome.value.inserted {
eprintln!(" Text inserted successfully.");
} else {
eprintln!(
" Insert failed: {}",
outcome.value.error.as_deref().unwrap_or("unknown error")
);
std::process::exit(1);
}
Ok(())
})
}
/// `openhuman text-input ghost <text>` — show ghost text overlay.
fn run_ghost(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!("Usage: openhuman text-input ghost <text> [--ttl <ms>] [-v]");
println!();
println!("Show ghost text overlay near the focused input field.");
println!();
println!(" --ttl <ms> Auto-dismiss after N milliseconds (default: 3000)");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
init_quiet_logging(opts.verbose);
let text = rest.join(" ");
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let params = crate::openhuman::text_input::ShowGhostTextParams {
text,
ttl_ms: Some(opts.ttl_ms),
bounds: None,
};
let outcome = crate::openhuman::text_input::rpc::show_ghost(params)
.await
.map_err(|e| anyhow::anyhow!(e))?;
if outcome.value.shown {
eprintln!(" Ghost text shown (ttl={}ms).", opts.ttl_ms);
} else {
eprintln!(
" Show ghost failed: {}",
outcome.value.error.as_deref().unwrap_or("unknown error")
);
std::process::exit(1);
}
Ok(())
})
}
/// `openhuman text-input dismiss` — dismiss the ghost text overlay.
fn run_dismiss(args: &[String]) -> Result<()> {
if args.iter().any(|a| is_help(a)) {
println!("Usage: openhuman text-input dismiss [-v]");
println!();
println!("Dismiss the ghost text overlay.");
return Ok(());
}
let (opts, _) = parse_opts(args)?;
init_quiet_logging(opts.verbose);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async {
let outcome = crate::openhuman::text_input::rpc::dismiss_ghost()
.await
.map_err(|e| anyhow::anyhow!(e))?;
if outcome.value.dismissed {
eprintln!(" Ghost text dismissed.");
}
Ok(())
})
}
// ---------------------------------------------------------------------------
// Minimal HTTP router
// ---------------------------------------------------------------------------
fn build_router() -> axum::Router {
use axum::routing::{get, post};
axum::Router::new()
.route("/health", get(health))
.route("/rpc", post(rpc))
}
async fn health() -> impl axum::response::IntoResponse {
axum::Json(serde_json::json!({ "ok": true, "mode": "text-input-dev" }))
}
async fn rpc(
axum::Json(req): axum::Json<crate::core::types::RpcRequest>,
) -> axum::response::Response {
use crate::core::types::{RpcError, RpcFailure, RpcSuccess};
use axum::response::IntoResponse;
let id = req.id.clone();
let state = crate::core::jsonrpc::default_state();
match crate::core::jsonrpc::invoke_method(state, req.method.as_str(), req.params).await {
Ok(value) => (
axum::http::StatusCode::OK,
axum::Json(RpcSuccess {
jsonrpc: "2.0",
id,
result: value,
}),
)
.into_response(),
Err(message) => (
axum::http::StatusCode::OK,
axum::Json(RpcFailure {
jsonrpc: "2.0",
id,
error: RpcError {
code: -32000,
message,
data: None,
},
}),
)
.into_response(),
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn init_quiet_logging(verbose: bool) {
if !verbose && std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "warn");
}
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
}
fn is_help(value: &str) -> bool {
matches!(value, "-h" | "--help" | "help")
}
fn print_help() {
println!("openhuman text-input — text input intelligence\n");
println!("Usage:");
println!(" openhuman text-input run [--port <u16>] [-v]");
println!(" openhuman text-input read [--bounds] [-v]");
println!(" openhuman text-input insert <text> [-v]");
println!(" openhuman text-input ghost <text> [--ttl <ms>] [-v]");
println!(" openhuman text-input dismiss [-v]");
println!();
println!("Subcommands:");
println!(" run Start a lightweight JSON-RPC server with text input methods");
println!(" read Read the currently focused text input field (JSON to stdout)");
println!(" insert Insert text into the focused field");
println!(" ghost Show ghost text overlay near the focused field");
println!(" dismiss Dismiss the ghost text overlay");
println!();
println!("Common options:");
println!(" --port <u16> Server port for 'run' (default: 7798)");
println!(" --bounds Include element bounds in 'read' output");
println!(" --ttl <ms> Ghost text auto-dismiss (default: 3000)");
println!(" -v, --verbose Enable debug logging");
}
-386
View File
@@ -1,386 +0,0 @@
//! `openhuman tree-summarizer` — CLI for the hierarchical summary tree.
//!
//! Ingest content, run summarization jobs, query the tree, and inspect
//! status from the terminal without starting the full app.
//!
//! Usage:
//! openhuman tree-summarizer ingest <namespace> [--content <text> | --file <path>] [-v]
//! openhuman tree-summarizer run <namespace> [-v]
//! openhuman tree-summarizer query <namespace> [<node_id>] [-v]
//! openhuman tree-summarizer status <namespace> [-v]
//! openhuman tree-summarizer rebuild <namespace> [-v]
use anyhow::Result;
/// Entry point for `openhuman tree-summarizer <subcommand>`.
pub fn run_tree_summarizer_command(args: &[String]) -> Result<()> {
if args.is_empty() || is_help(&args[0]) {
print_help();
return Ok(());
}
match args[0].as_str() {
"ingest" => run_ingest(&args[1..]),
"run" => run_summarize(&args[1..]),
"query" => run_query(&args[1..]),
"status" => run_status(&args[1..]),
"rebuild" => run_rebuild(&args[1..]),
other => Err(anyhow::anyhow!(
"unknown tree-summarizer subcommand '{other}'. Run `openhuman tree-summarizer --help`."
)),
}
}
// ---------------------------------------------------------------------------
// Option parsing
// ---------------------------------------------------------------------------
struct CliOpts {
verbose: bool,
content: Option<String>,
file: Option<String>,
node_id: Option<String>,
}
fn parse_opts(args: &[String]) -> Result<(CliOpts, Vec<String>)> {
let mut verbose = false;
let mut content: Option<String> = None;
let mut file: Option<String> = None;
let mut node_id: Option<String> = None;
let mut rest = Vec::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--content" | "-c" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --content"))?;
content = Some(val.clone());
i += 2;
}
"--file" | "-f" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --file"))?;
file = Some(val.clone());
i += 2;
}
"--node-id" | "--node" => {
let val = args
.get(i + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --node-id"))?;
node_id = Some(val.clone());
i += 2;
}
"-v" | "--verbose" => {
verbose = true;
i += 1;
}
"-h" | "--help" => {
rest.push(args[i].clone());
i += 1;
}
_ => {
rest.push(args[i].clone());
i += 1;
}
}
}
Ok((
CliOpts {
verbose,
content,
file,
node_id,
},
rest,
))
}
// ---------------------------------------------------------------------------
// Subcommands
// ---------------------------------------------------------------------------
/// `openhuman tree-summarizer ingest <namespace> --content <text>` or `--file <path>`
fn run_ingest(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!("Usage: openhuman tree-summarizer ingest <namespace> [--content <text>] [--file <path>] [-v]");
println!();
println!("Append content to the summarization buffer for a namespace.");
println!();
println!(" <namespace> Target namespace for the summary tree");
println!(" --content, -c <text> Raw text content to ingest");
println!(" --file, -f <path> Read content from a file (use - for stdin)");
println!(" -v, --verbose Enable debug logging");
println!();
println!("Either --content or --file is required. If both are given, --file wins.");
return Ok(());
}
let namespace = &rest[0];
let content = if let Some(ref path) = opts.file {
if path == "-" {
use std::io::Read;
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.map_err(|e| anyhow::anyhow!("failed to read stdin: {e}"))?;
buf
} else {
std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("failed to read '{}': {e}", path))?
}
} else if let Some(ref text) = opts.content {
text.clone()
} else {
return Err(anyhow::anyhow!(
"either --content or --file is required. Run `openhuman tree-summarizer ingest --help`."
));
};
if content.trim().is_empty() {
return Err(anyhow::anyhow!("content is empty"));
}
init_logging(opts.verbose);
let rt = build_runtime()?;
rt.block_on(async {
let config = load_config().await?;
let outcome = crate::openhuman::tree_summarizer::rpc::tree_summarizer_ingest(
&config, namespace, &content, None, None,
)
.await
.map_err(anyhow::Error::msg)?;
println!(
"{}",
serde_json::to_string_pretty(&outcome.value)
.unwrap_or_else(|_| format!("{:?}", outcome.value))
);
Ok(())
})
}
/// `openhuman tree-summarizer run <namespace>`
fn run_summarize(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!("Usage: openhuman tree-summarizer run <namespace> [-v]");
println!();
println!("Trigger the summarization job for a namespace.");
println!("Drains the buffer, creates the hour leaf, and propagates upward.");
println!();
println!(" <namespace> Target namespace");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let namespace = &rest[0];
init_logging(opts.verbose);
let rt = build_runtime()?;
rt.block_on(async {
let config = load_config().await?;
let outcome =
crate::openhuman::tree_summarizer::rpc::tree_summarizer_run(&config, namespace)
.await
.map_err(anyhow::Error::msg)?;
println!(
"{}",
serde_json::to_string_pretty(&outcome.value)
.unwrap_or_else(|_| format!("{:?}", outcome.value))
);
Ok(())
})
}
/// `openhuman tree-summarizer query <namespace> [<node_id>]`
fn run_query(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!(
"Usage: openhuman tree-summarizer query <namespace> [<node_id>] [--node-id <id>] [-v]"
);
println!();
println!("Read a summary tree node and its direct children.");
println!();
println!(" <namespace> Target namespace");
println!(" <node_id> Node ID to query (default: root)");
println!(" --node-id, --node Alternative way to specify the node ID");
println!(" -v, --verbose Enable debug logging");
println!();
println!("Node ID examples:");
println!(" root All-time summary");
println!(" 2024 Year summary");
println!(" 2024/03 Month summary");
println!(" 2024/03/15 Day summary");
println!(" 2024/03/15/14 Hour leaf (2pm)");
return Ok(());
}
let namespace = &rest[0];
let node_id = opts
.node_id
.as_deref()
.or_else(|| rest.get(1).map(|s| s.as_str()));
init_logging(opts.verbose);
let rt = build_runtime()?;
rt.block_on(async {
let config = load_config().await?;
let outcome = crate::openhuman::tree_summarizer::rpc::tree_summarizer_query(
&config, namespace, node_id,
)
.await
.map_err(anyhow::Error::msg)?;
println!(
"{}",
serde_json::to_string_pretty(&outcome.value)
.unwrap_or_else(|_| format!("{:?}", outcome.value))
);
Ok(())
})
}
/// `openhuman tree-summarizer status <namespace>`
fn run_status(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!("Usage: openhuman tree-summarizer status <namespace> [-v]");
println!();
println!("Show tree metadata: node count, depth, date range.");
println!();
println!(" <namespace> Target namespace");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let namespace = &rest[0];
init_logging(opts.verbose);
let rt = build_runtime()?;
rt.block_on(async {
let config = load_config().await?;
let outcome =
crate::openhuman::tree_summarizer::rpc::tree_summarizer_status(&config, namespace)
.await
.map_err(anyhow::Error::msg)?;
println!(
"{}",
serde_json::to_string_pretty(&outcome.value)
.unwrap_or_else(|_| format!("{:?}", outcome.value))
);
Ok(())
})
}
/// `openhuman tree-summarizer rebuild <namespace>`
fn run_rebuild(args: &[String]) -> Result<()> {
let (opts, rest) = parse_opts(args)?;
if rest.iter().any(|a| is_help(a)) || rest.is_empty() {
println!("Usage: openhuman tree-summarizer rebuild <namespace> [-v]");
println!();
println!("Rebuild the entire summary tree from hour leaves upward.");
println!("This re-summarizes all intermediate levels (day, month, year, root).");
println!();
println!(" <namespace> Target namespace");
println!(" -v, --verbose Enable debug logging");
return Ok(());
}
let namespace = &rest[0];
init_logging(opts.verbose);
eprintln!(" Rebuilding tree for namespace '{namespace}'... this may take a while.");
let rt = build_runtime()?;
rt.block_on(async {
let config = load_config().await?;
let outcome =
crate::openhuman::tree_summarizer::rpc::tree_summarizer_rebuild(&config, namespace)
.await
.map_err(anyhow::Error::msg)?;
println!(
"{}",
serde_json::to_string_pretty(&outcome.value)
.unwrap_or_else(|_| format!("{:?}", outcome.value))
);
Ok(())
})
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn build_runtime() -> Result<tokio::runtime::Runtime> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|e| anyhow::anyhow!("failed to build tokio runtime: {e}"))
}
async fn load_config() -> Result<crate::openhuman::config::Config> {
let mut config = crate::openhuman::config::Config::load_or_init()
.await
.unwrap_or_default();
config.apply_env_overrides();
Ok(config)
}
fn init_logging(verbose: bool) {
if !verbose && std::env::var_os("RUST_LOG").is_none() {
unsafe { std::env::set_var("RUST_LOG", "warn") };
}
crate::core::logging::init_for_cli_run(verbose, crate::core::logging::CliLogDefault::Global);
}
fn is_help(value: &str) -> bool {
matches!(value, "-h" | "--help" | "help")
}
fn print_help() {
println!("openhuman tree-summarizer — hierarchical summary tree\n");
println!("Usage:");
println!(
" openhuman tree-summarizer ingest <namespace> [--content <text>] [--file <path>] [-v]"
);
println!(" openhuman tree-summarizer run <namespace> [-v]");
println!(" openhuman tree-summarizer query <namespace> [<node_id>] [-v]");
println!(" openhuman tree-summarizer status <namespace> [-v]");
println!(" openhuman tree-summarizer rebuild <namespace> [-v]");
println!();
println!("Subcommands:");
println!(" ingest Buffer raw content for the next summarization run");
println!(" run Drain buffer → create hour leaf → propagate summaries upward");
println!(" query Read a node and its children (default: root)");
println!(" status Show tree metadata (node count, depth, date range)");
println!(" rebuild Rebuild entire tree from hour leaves (re-summarizes all levels)");
println!();
println!("Common options:");
println!(" -v, --verbose Enable debug logging");
println!();
println!("Examples:");
println!(" openhuman tree-summarizer ingest my-ns --content 'Some raw data to summarize'");
println!(" openhuman tree-summarizer ingest my-ns --file notes.txt");
println!(" cat journal.md | openhuman tree-summarizer ingest my-ns --file -");
println!(" openhuman tree-summarizer run my-ns");
println!(" openhuman tree-summarizer query my-ns root");
println!(" openhuman tree-summarizer query my-ns 2024/03/15");
println!(" openhuman tree-summarizer status my-ns");
}