use clap::Parser;
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use hematite::runtime::{
build_runtime_bundle, run_agent_loop, spawn_runtime_profile_sync, AgentLoopConfig,
AgentLoopRuntime, RuntimeBundle,
};
use hematite::{ui, CliCockpit};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::sync::Arc;
fn wants_version_report(args: &[String]) -> bool {
args.len() == 2 && matches!(args[1].as_str(), "--version" | "-V")
}
fn report_indicates_issues(content: &str) -> bool {
hematite::agent::report_export::report_has_issues_in_content(content)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
hematite::tools::hardening::pre_main_hardening();
let raw_args: Vec<String> = std::env::args().collect();
if wants_version_report(&raw_args) {
println!("{}", hematite::hematite_version_report());
return Ok(());
}
let cwd_ok = std::env::current_dir()
.map(|p| std::fs::read_dir(&p).is_ok())
.unwrap_or(false);
if !cwd_ok {
let home = std::env::var_os("USERPROFILE")
.or_else(|| std::env::var_os("HOME"))
.map(std::path::PathBuf::from);
if let Some(home) = home {
let _ = std::env::set_current_dir(home);
}
}
let cockpit = CliCockpit::parse();
if cockpit.mcp_server {
let edge = cockpit.edge_redact || cockpit.semantic_redact;
let semantic = cockpit.semantic_redact;
let semantic_url = cockpit.semantic_url.as_deref().unwrap_or(&cockpit.url);
let semantic_model = cockpit.semantic_model.as_deref().unwrap_or("");
hematite::agent::mcp_server::run_mcp_server(
edge,
semantic,
&cockpit.url,
semantic_url,
semantic_model,
)
.await?;
return Ok(());
}
if cockpit.report {
let fmt = cockpit.report_format.trim().to_ascii_lowercase();
if cockpit.open {
let path = match fmt.as_str() {
"json" => hematite::agent::report_export::save_report_json().await.1,
"html" => hematite::agent::report_export::save_report_html().await.1,
_ => {
hematite::agent::report_export::save_report_markdown()
.await
.1
}
};
println!("Report saved: {}", path.display());
open_path(&path);
} else {
let out = match fmt.as_str() {
"json" => hematite::agent::report_export::generate_report_json().await,
"html" => hematite::agent::report_export::generate_report_html().await,
_ => hematite::agent::report_export::generate_report_markdown().await,
};
print!("{}", out);
}
return Ok(());
}
if cockpit.diagnose {
let fmt = cockpit.report_format.trim().to_ascii_lowercase();
let (content, path) = match fmt.as_str() {
"html" => hematite::agent::report_export::save_diagnosis_report_html().await,
_ => hematite::agent::report_export::save_diagnosis_report().await,
};
println!("Diagnosis saved: {}", path.display());
if cockpit.open {
open_path(&path);
}
std::process::exit(if report_indicates_issues(&content) {
1
} else {
0
});
}
if let Some(ref preset) = cockpit.triage {
let preset_str = preset.as_str();
let fmt = cockpit.report_format.trim().to_ascii_lowercase();
let (content, path) = match fmt.as_str() {
"html" => hematite::agent::report_export::save_triage_report_html(preset_str).await,
_ => hematite::agent::report_export::save_triage_report(preset_str).await,
};
println!("Triage saved: {}", path.display());
if cockpit.open {
open_path(&path);
}
std::process::exit(if report_indicates_issues(&content) {
1
} else {
0
});
}
if let Some(ref issue) = cockpit.fix {
let issue_str = issue.trim();
if issue_str.eq_ignore_ascii_case("list") || issue_str.eq_ignore_ascii_case("help") {
println!("hematite --fix: supported issue categories\n");
for (category, keywords) in hematite::agent::report_export::fix_issue_categories() {
println!(" {:<22} {}", category, keywords);
}
println!("\nExamples:");
println!(" hematite --fix \"PC running slow\"");
println!(" hematite --fix \"can't connect to internet\" --report-format html --open");
println!(" hematite --fix \"BSOD after update\"");
println!(" hematite --fix \"Outlook not opening\"");
return Ok(());
}
if cockpit.dry_run {
let topics = hematite::agent::report_export::fix_plan_topics(issue_str);
println!("hematite --fix \"{}\": would inspect:\n", issue_str);
for (i, (topic, label)) in topics.iter().enumerate() {
println!(" [{}/{}] {} ({})", i + 1, topics.len(), label, topic);
}
println!("\nUp to 3 follow-up checks may be added automatically based on findings.");
return Ok(());
}
let fmt = cockpit.report_format.trim().to_ascii_lowercase();
let (content, path) = match fmt.as_str() {
"html" => hematite::agent::report_export::save_fix_plan_html(issue).await,
_ => hematite::agent::report_export::save_fix_plan(issue).await,
};
println!("Fix plan saved: {}", path.display());
if cockpit.open {
open_path(&path);
}
if cockpit.execute {
let auto_cmds = hematite::agent::report_export::fix_plan_auto_commands(&content);
if auto_cmds.is_empty() {
println!("\nNo safe auto-fixes available for these findings.");
} else {
println!("\nFound {} safe auto-fix(es):", auto_cmds.len());
for (i, (label, cmd)) in auto_cmds.iter().enumerate() {
println!(" [{}] {} — {}", i + 1, label, cmd);
}
print!("\nRun these now? [Y/n]: ");
use std::io::Write;
let _ = std::io::stdout().flush();
let mut answer = String::new();
let _ = std::io::stdin().read_line(&mut answer);
if !answer.trim().eq_ignore_ascii_case("n") {
println!();
for (label, cmd) in &auto_cmds {
print!(" Running: {}... ", label);
let _ = std::io::stdout().flush();
let result = std::process::Command::new("cmd")
.args(["/C", cmd])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
match result {
Ok(s) if s.success() => println!("OK"),
Ok(s) => println!("Failed (code {})", s.code().unwrap_or(1)),
Err(e) => println!("Error: {}", e),
}
}
println!("\nSafe fixes complete.");
}
}
}
std::process::exit(if report_indicates_issues(&content) {
1
} else {
0
});
}
if let Some(ref cadence) = cockpit.schedule {
let cadence_str = cadence.trim();
if cadence_str == "status" {
println!("{}", hematite::agent::scheduler::query_scheduled_task());
return Ok(());
}
if cadence_str == "remove" {
match hematite::agent::scheduler::remove_scheduled_task() {
Ok(msg) => println!("{}", msg),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
return Ok(());
}
let exe_path = std::env::current_exe()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "hematite".to_string());
match hematite::agent::scheduler::register_scheduled_task(cadence_str, &exe_path) {
Ok(msg) => println!("{}", msg),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
return Ok(());
}
if let Some(path) = cockpit.pdf_extract_helper.as_deref() {
let code = hematite::memory::vein::run_pdf_extract_helper(std::path::Path::new(path));
std::process::exit(code);
}
let local_soul = ui::hatch::generate_soul(cockpit.reroll.clone());
if cockpit.stats {
println!(
"Species: {} | Wisdom: {} | Chaos: {}",
local_soul.species, local_soul.wisdom, local_soul.chaos
);
return Ok(());
}
let RuntimeBundle {
services,
channels,
watcher_guard: _watcher_guard,
} = build_runtime_bundle(
&cockpit,
&local_soul.species,
local_soul.snark,
!cockpit.rusty,
)
.await?;
let hematite::runtime::RuntimeServices {
engine,
gpu_state,
git_state,
voice_manager,
swarm_coordinator,
cancel_token,
searx_session,
} = services;
let hematite::runtime::RuntimeChannels {
specular_rx,
agent_tx,
agent_rx,
swarm_tx,
swarm_rx,
user_input_tx,
user_input_rx,
} = channels;
let prewarm_engine = engine.clone();
tokio::spawn(async move {
let _ = prewarm_engine.prewarm().await;
});
let tui_cancel_token = cancel_token.clone();
tokio::spawn(run_agent_loop(
AgentLoopRuntime {
user_input_rx,
agent_tx: agent_tx.clone(),
services: hematite::runtime::RuntimeServices {
engine: engine.clone(),
gpu_state: gpu_state.clone(),
git_state: git_state.clone(),
voice_manager: voice_manager.clone(),
swarm_coordinator: swarm_coordinator.clone(),
cancel_token,
searx_session: searx_session.clone(),
},
},
AgentLoopConfig {
yolo: cockpit.yolo,
professional: !cockpit.rusty,
brief: cockpit.brief,
snark: local_soul.snark,
chaos: local_soul.chaos,
soul_personality: local_soul.personality.clone(),
fast_model: cockpit.fast_model.clone(),
think_model: cockpit.think_model.clone(),
},
));
let _runtime_profile_poller = spawn_runtime_profile_sync(engine.clone(), agent_tx.clone());
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
std::io::stdout().execute(EnterAlternateScreen)?;
std::io::stdout().execute(crossterm::event::EnableMouseCapture)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
let _app_result = ui::tui::run_app(
&mut terminal,
specular_rx,
agent_rx,
user_input_tx,
swarm_rx,
swarm_tx,
swarm_coordinator,
Arc::new(std::sync::Mutex::new(std::time::Instant::now())),
cockpit.clone(),
local_soul,
!cockpit.rusty,
gpu_state,
git_state,
tui_cancel_token,
voice_manager,
)
.await;
disable_raw_mode()?;
std::io::stdout().execute(crossterm::event::DisableMouseCapture)?;
std::io::stdout().execute(LeaveAlternateScreen)?;
#[cfg(target_os = "windows")]
{
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
fn FlushConsoleInputBuffer(hConsoleInput: *mut std::ffi::c_void) -> i32;
}
const STD_INPUT_HANDLE: u32 = 0xFFFFFFF6; unsafe {
let h = GetStdHandle(STD_INPUT_HANDLE);
if !h.is_null() && h as isize != -1 {
FlushConsoleInputBuffer(h);
}
}
}
if let Some(summary) =
hematite::agent::searx_lifecycle::shutdown_searx_if_owned(&searx_session).await
{
eprintln!("{}", summary);
}
Ok(())
}
fn open_path(path: &std::path::Path) {
#[cfg(target_os = "windows")]
{
let s = path.to_string_lossy().into_owned();
let _ = std::process::Command::new("cmd")
.args(["/c", "start", "", &s])
.spawn();
}
#[cfg(not(target_os = "windows"))]
{
let opener = if cfg!(target_os = "macos") {
"open"
} else {
"xdg-open"
};
let _ = std::process::Command::new(opener).arg(path).spawn();
}
}
#[cfg(test)]
mod tests {
use super::wants_version_report;
#[test]
fn detects_plain_version_flag() {
assert!(wants_version_report(&[
"hematite".into(),
"--version".into()
]));
assert!(wants_version_report(&["hematite".into(), "-V".into()]));
assert!(!wants_version_report(&["hematite".into()]));
assert!(!wants_version_report(&[
"hematite".into(),
"--version".into(),
"--brief".into()
]));
}
}