Skip to main content

hematite/
lib.rs

1pub mod agent;
2
3/// Serializes tests that call `std::env::set_current_dir` to prevent race conditions
4/// in parallel test runs. Any test that mutates the process-wide cwd must hold this
5/// lock for the duration of the directory change.
6#[cfg(test)]
7pub(crate) static TEST_CWD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
8
9pub mod memory;
10pub mod runtime;
11pub mod telemetry;
12pub mod tools;
13pub mod ui;
14
15pub const HEMATITE_VERSION: &str = env!("CARGO_PKG_VERSION");
16pub const HEMATITE_AUTHOR: &str = "Ocean Bennett";
17pub const HEMATITE_REPOSITORY_URL: &str = "https://github.com/undergroundrap/hematite-cli";
18pub const HEMATITE_SHORT_DESCRIPTION: &str =
19    "Local-first AI coding harness — Senior SysAdmin, Network Admin, Data Analyst, and Software Engineer in your terminal.";
20const HEMATITE_GIT_COMMIT_SHORT_RAW: &str = env!("HEMATITE_GIT_COMMIT_SHORT");
21const HEMATITE_GIT_EXACT_TAG_RAW: &str = env!("HEMATITE_GIT_EXACT_TAG");
22const HEMATITE_GIT_DIRTY_RAW: &str = env!("HEMATITE_GIT_DIRTY");
23
24pub fn hematite_git_commit_short() -> Option<&'static str> {
25    #[allow(clippy::const_is_empty)]
26    (!HEMATITE_GIT_COMMIT_SHORT_RAW.is_empty()).then_some(HEMATITE_GIT_COMMIT_SHORT_RAW)
27}
28
29pub fn hematite_git_exact_tag() -> Option<&'static str> {
30    #[allow(clippy::const_is_empty)]
31    (!HEMATITE_GIT_EXACT_TAG_RAW.is_empty()).then_some(HEMATITE_GIT_EXACT_TAG_RAW)
32}
33
34pub fn hematite_git_dirty() -> bool {
35    HEMATITE_GIT_DIRTY_RAW.eq_ignore_ascii_case("true")
36}
37
38pub fn hematite_build_descriptor() -> String {
39    let release_tag = format!("v{}", HEMATITE_VERSION);
40    let exact_release = matches!(hematite_git_exact_tag(), Some(tag) if tag == release_tag);
41
42    if exact_release && !hematite_git_dirty() {
43        "release".to_string()
44    } else {
45        match (hematite_git_commit_short(), hematite_git_dirty()) {
46            (Some(commit), true) => format!("dev+{}-dirty", commit),
47            (Some(commit), false) => format!("dev+{}", commit),
48            (None, true) => "dev-dirty".to_string(),
49            (None, false) => "dev".to_string(),
50        }
51    }
52}
53
54pub fn hematite_version_display() -> String {
55    format!("v{} [{}]", HEMATITE_VERSION, hematite_build_descriptor())
56}
57
58pub fn hematite_version_report() -> String {
59    let mut lines = vec![
60        format!("Hematite v{}", HEMATITE_VERSION),
61        format!("Build: {}", hematite_build_descriptor()),
62    ];
63    if let Some(commit) = hematite_git_commit_short() {
64        lines.push(format!("Commit: {}", commit));
65    }
66    lines.push(format!(
67        "Built from a dirty worktree: {}",
68        if hematite_git_dirty() { "yes" } else { "no" }
69    ));
70    lines.push(format!(
71        "Exact release tag at build time: {}",
72        hematite_git_exact_tag().unwrap_or("none")
73    ));
74    lines.join("\n")
75}
76
77pub fn hematite_about_report() -> String {
78    [
79        format!("Hematite v{}", HEMATITE_VERSION),
80        format!("Build: {}", hematite_build_descriptor()),
81        format!("Created and maintained by {}", HEMATITE_AUTHOR),
82        HEMATITE_SHORT_DESCRIPTION.to_string(),
83        format!("Repo: {}", HEMATITE_REPOSITORY_URL),
84    ]
85    .join("\n")
86}
87
88pub fn hematite_identity_answer() -> String {
89    format!(
90        "Hematite was created and is maintained by {}.\n\n{}\n\nThe running assistant uses a local model runtime, but Hematite itself is the local harness: the TUI, tool use, file editing, workflow control, host inspection, data analysis sandbox, voice integration, and workstation-assistant architecture.\n\nRepo: {}",
91        HEMATITE_AUTHOR, HEMATITE_SHORT_DESCRIPTION, HEMATITE_REPOSITORY_URL
92    )
93}
94
95// Standard imports for library users
96pub use agent::config::HematiteConfig;
97pub use agent::conversation::ConversationManager;
98pub use agent::inference::InferenceEngine;
99
100use clap::Parser;
101
102#[derive(Parser, Debug, Clone)]
103#[command(
104    author,
105    version,
106    about = "Hematite CLI - SysAdmin, Network Admin, Data Analyst, and Software Engineer in your terminal",
107    long_about = None
108)]
109pub struct CliCockpit {
110    #[arg(long, help = "Bypasses the high-risk modal (Danger mode)")]
111    pub yolo: bool,
112
113    #[arg(
114        long,
115        default_value_t = 3,
116        help = "Sets max parallel workers (default 3)"
117    )]
118    pub swarm_size: usize,
119
120    #[arg(
121        long,
122        help = "Forces the Vigil Brief Mode for concise, high-speed output"
123    )]
124    pub brief: bool,
125
126    #[arg(
127        long,
128        help = "Pass a custom salt to reroll the deterministic species hash"
129    )]
130    pub reroll: Option<String>,
131
132    #[arg(
133        long,
134        help = "Rusty Mode: Enables the Rusty personality system, snark, and companion features"
135    )]
136    pub rusty: bool,
137
138    #[arg(long, help = "Show Rusty stats and exit")]
139    pub stats: bool,
140
141    #[arg(
142        long,
143        help = "Skip the blocking splash screen and enter the TUI immediately"
144    )]
145    pub no_splash: bool,
146
147    #[arg(
148        long,
149        help = "Optional model ID for simple tasks (overrides auto-detect)"
150    )]
151    pub fast_model: Option<String>,
152
153    #[arg(
154        long,
155        help = "Optional model ID for complex tasks (overrides auto-detect)"
156    )]
157    pub think_model: Option<String>,
158
159    #[arg(
160        long,
161        default_value = "http://localhost:1234/v1",
162        help = "The base URL for the OpenAI-compatible API"
163    )]
164    pub url: String,
165
166    #[arg(
167        long,
168        help = "Run as an MCP stdio server — exposes inspect_host to Claude Desktop, OpenClaw, Cursor, and any MCP-capable agent"
169    )]
170    pub mcp_server: bool,
171
172    #[arg(
173        long,
174        help = "Enable edge redaction in MCP server mode — strips usernames, MACs, serial numbers, hostnames, and credentials before responses leave the machine"
175    )]
176    pub edge_redact: bool,
177
178    #[arg(
179        long,
180        help = "Enable semantic edge redaction — routes inspect_host output through the local model for privacy-safe summarization before any data leaves the machine. Requires a local OpenAI-compatible runtime running. Implies --edge-redact."
181    )]
182    pub semantic_redact: bool,
183
184    #[arg(
185        long,
186        help = "Endpoint for --semantic-redact (default: same as --url). Point at a dedicated compact model, e.g. Bonsai 8B on port 1235, while your main model stays on 1234."
187    )]
188    pub semantic_url: Option<String>,
189
190    #[arg(
191        long,
192        help = "Model ID for --semantic-redact (e.g. bonsai-8b). Required when multiple models are loaded in the local runtime. Omit for single-model setups."
193    )]
194    pub semantic_model: Option<String>,
195
196    #[arg(
197        long,
198        help = "Run a headless diagnostic report and print to stdout — no TUI launched. Pipe to a file: hematite --report > health.md"
199    )]
200    pub report: bool,
201
202    #[arg(
203        long,
204        default_value = "md",
205        help = "Output format for --report: 'md' (markdown, default), 'json', or 'html' (self-contained, double-clickable)"
206    )]
207    pub report_format: String,
208
209    #[arg(
210        long,
211        help = "Run a full staged triage — no TUI, no model required. Saves diagnosis to .hematite/reports/ and prints the path. Add --open to launch the file immediately."
212    )]
213    pub diagnose: bool,
214
215    #[arg(
216        long,
217        default_missing_value = "default",
218        num_args = 0..=1,
219        value_name = "PRESET",
220        help = "IT-first-look triage — no model required. Optional preset: network, security, performance, storage, apps. Plain --triage runs the IT-first-look default (health, security, connectivity, identity, updates). Saves to .hematite/reports/triage-DATE. Add --open for html."
221    )]
222    pub triage: Option<String>,
223
224    #[arg(
225        long,
226        value_name = "ISSUE",
227        help = "Generate a targeted fix plan for a stated issue — no model required. Keyword-matches your issue to the relevant inspect_host topics, runs them, and saves a step-by-step fix plan. Example: hematite --fix \"PC running slow\""
228    )]
229    pub fix: Option<String>,
230
231    #[arg(
232        long,
233        help = "After generating a --report, --diagnose, --triage, or --fix report, open the saved file in the default application (browser for HTML, editor for Markdown)"
234    )]
235    pub open: bool,
236
237    #[arg(
238        long,
239        help = "With --fix: preview which topics would be inspected without running any checks"
240    )]
241    pub dry_run: bool,
242
243    #[arg(
244        long,
245        help = "With --fix: after the fix plan is generated, offer to run any safe non-destructive fixes automatically (service restarts, DNS flush, clock sync, etc.)"
246    )]
247    pub execute: bool,
248
249    #[arg(
250        long,
251        default_missing_value = "weekly",
252        num_args = 0..=1,
253        value_name = "CADENCE",
254        help = "Register a Windows scheduled task that runs --triage automatically. \
255                CADENCE: weekly (default, Monday 08:00), daily (08:00), \
256                remove (unregister), status (show current state). \
257                Example: hematite --schedule  or  hematite --schedule daily"
258    )]
259    pub schedule: Option<String>,
260
261    #[arg(long, hide = true)]
262    pub pdf_extract_helper: Option<String>,
263
264    #[arg(long, hide = true)]
265    pub teleported_from: Option<String>,
266}
267
268#[cfg(test)]
269mod tests {
270    #[test]
271    fn version_report_contains_release_version() {
272        let report = crate::hematite_version_report();
273        assert!(report.contains(crate::HEMATITE_VERSION));
274        assert!(report.contains("Build:"));
275    }
276
277    #[test]
278    fn about_report_contains_author_and_repo() {
279        let report = crate::hematite_about_report();
280        assert!(report.contains(crate::HEMATITE_AUTHOR));
281        assert!(report.contains(crate::HEMATITE_REPOSITORY_URL));
282    }
283}