mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
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:
co-authored by
Claude Sonnet 4.6
Jwalin Shah
Steven Enamakel
parent
8212438112
commit
325f019853
+4
-97
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user