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    // ── MCP Server ────────────────────────────────────────────────────────────
167    #[arg(
168        long,
169        help_heading = "MCP Server",
170        help = "Run as an MCP stdio server — exposes inspect_host to Claude Desktop, OpenClaw, Cursor, and any MCP-capable agent"
171    )]
172    pub mcp_server: bool,
173
174    #[arg(
175        long,
176        help_heading = "MCP Server",
177        help = "Enable edge redaction in MCP server mode — strips usernames, MACs, serial numbers, hostnames, and credentials before responses leave the machine"
178    )]
179    pub edge_redact: bool,
180
181    #[arg(
182        long,
183        help_heading = "MCP Server",
184        help = "Enable semantic edge redaction — routes inspect_host output through the local model for privacy-safe summarization before any data leaves the machine. Implies --edge-redact."
185    )]
186    pub semantic_redact: bool,
187
188    #[arg(
189        long,
190        help_heading = "MCP Server",
191        help = "Endpoint for --semantic-redact (default: same as --url). Point at a dedicated compact model on a different port."
192    )]
193    pub semantic_url: Option<String>,
194
195    #[arg(
196        long,
197        help_heading = "MCP Server",
198        help = "Model ID for --semantic-redact (e.g. bonsai-8b). Required when multiple models are loaded."
199    )]
200    pub semantic_model: Option<String>,
201
202    // ── Headless Reports ──────────────────────────────────────────────────────
203    #[arg(
204        long,
205        help_heading = "Headless Reports",
206        help = "Run a headless diagnostic report and print to stdout — no TUI launched. Pipe to a file: hematite --report > health.md"
207    )]
208    pub report: bool,
209
210    #[arg(
211        long,
212        help_heading = "Headless Reports",
213        default_value = "md",
214        help = "Output format: md (default), json, or html (self-contained, double-clickable)"
215    )]
216    pub report_format: String,
217
218    #[arg(
219        long,
220        help_heading = "Headless Reports",
221        help = "Staged triage — health_report then targeted follow-up inspections. Saves to .hematite/reports/. Add --open to launch."
222    )]
223    pub diagnose: bool,
224
225    #[arg(
226        long,
227        help_heading = "Headless Reports",
228        default_missing_value = "default",
229        num_args = 0..=1,
230        value_name = "PRESET",
231        help = "IT-first-look triage. Optional preset: network, security, performance, storage, apps. Plain --triage runs health+security+connectivity+identity+updates."
232    )]
233    pub triage: Option<String>,
234
235    #[arg(
236        long,
237        help_heading = "Headless Reports",
238        value_name = "ISSUE",
239        help = "Targeted fix plan — keyword-matches your issue to the right inspect_host topics and saves a step-by-step plan. Example: hematite --fix \"PC running slow\""
240    )]
241    pub fix: Option<String>,
242
243    #[arg(
244        long,
245        help_heading = "Headless Reports",
246        help = "Open the saved report file immediately after writing (browser for HTML, editor for Markdown)"
247    )]
248    pub open: bool,
249
250    #[arg(
251        long,
252        help_heading = "Headless Reports",
253        help = "With --fix: preview which topics would be inspected without running any checks"
254    )]
255    pub dry_run: bool,
256
257    #[arg(
258        long,
259        help_heading = "Headless Reports",
260        help = "With --fix: offer to run safe auto-fixes after generating the plan (DNS flush, service restarts, clock sync, etc.)"
261    )]
262    pub execute: bool,
263
264    #[arg(
265        long,
266        help_heading = "Headless Reports",
267        default_missing_value = "weekly",
268        num_args = 0..=1,
269        value_name = "CADENCE",
270        help = "Register a Windows scheduled task for --triage. CADENCE: weekly (default), daily, remove, status."
271    )]
272    pub schedule: Option<String>,
273
274    // ── Modelless Inspection ──────────────────────────────────────────────────
275    #[arg(
276        long,
277        help_heading = "Modelless Inspection",
278        help = "List all 128 available inspect_host topics by category. No model or TUI required."
279    )]
280    pub inventory: bool,
281
282    #[arg(
283        long,
284        help_heading = "Modelless Inspection",
285        value_name = "TOPIC[,TOPIC2,...]",
286        help = "Run any inspect_host topic directly to stdout. Comma-separate for multiple topics. Example: hematite --inspect wifi,latency,dns_cache"
287    )]
288    pub inspect: Option<String>,
289
290    #[arg(
291        long,
292        help_heading = "Modelless Inspection",
293        value_name = "QUERY",
294        help = "Natural-language query routed to the right inspect_host topics. Example: hematite --query \"why is my PC slow\""
295    )]
296    pub query: Option<String>,
297
298    #[arg(
299        long,
300        help_heading = "Modelless Inspection",
301        value_name = "TOPIC[,TOPIC2,...]",
302        help = "Continuously poll topic(s) every N seconds (see --watch-interval). Press Ctrl+C to stop. Example: hematite --watch resource_load,thermal"
303    )]
304    pub watch: Option<String>,
305
306    #[arg(
307        long,
308        help_heading = "Modelless Inspection",
309        value_name = "SECONDS",
310        default_value = "5",
311        help = "Polling interval in seconds for --watch (default: 5)"
312    )]
313    pub watch_interval: u64,
314
315    #[arg(
316        long,
317        help_heading = "Modelless Inspection",
318        value_name = "TOPIC[,TOPIC2,...]",
319        help = "Take two snapshots separated by --diff-after seconds and show a colored diff. Example: hematite --diff processes --diff-after 60"
320    )]
321    pub diff: Option<String>,
322
323    #[arg(
324        long,
325        help_heading = "Modelless Inspection",
326        value_name = "SECONDS",
327        default_value = "30",
328        help = "Seconds between snapshots for --diff (default: 30)"
329    )]
330    pub diff_after: u64,
331
332    #[arg(
333        long,
334        help_heading = "Modelless Inspection",
335        value_name = "PATTERN",
336        help = "With --watch: silent heartbeat when pattern is absent, bell + full output on match. Example: hematite --watch thermal --alert throttl"
337    )]
338    pub alert: Option<String>,
339
340    #[arg(
341        long,
342        help_heading = "Modelless Inspection",
343        value_name = "NAME",
344        help = "With --inspect: save output to .hematite/snapshots/<name>.txt instead of printing. Example: hematite --inspect thermal --snapshot before-update"
345    )]
346    pub snapshot: Option<String>,
347
348    #[arg(
349        long,
350        help_heading = "Modelless Inspection",
351        value_name = "NAME",
352        help = "With --diff: load snapshot A from .hematite/snapshots/<name>.txt instead of running a live capture. Example: hematite --diff thermal --from before-update"
353    )]
354    pub from: Option<String>,
355
356    #[arg(
357        long,
358        help_heading = "Modelless Inspection",
359        help = "List saved snapshots in .hematite/snapshots/ with timestamps and sizes"
360    )]
361    pub snapshots: bool,
362
363    #[arg(long, hide = true)]
364    pub pdf_extract_helper: Option<String>,
365
366    #[arg(long, hide = true)]
367    pub teleported_from: Option<String>,
368}
369
370#[cfg(test)]
371mod tests {
372    #[test]
373    fn version_report_contains_release_version() {
374        let report = crate::hematite_version_report();
375        assert!(report.contains(crate::HEMATITE_VERSION));
376        assert!(report.contains("Build:"));
377    }
378
379    #[test]
380    fn about_report_contains_author_and_repo() {
381        let report = crate::hematite_about_report();
382        assert!(report.contains(crate::HEMATITE_AUTHOR));
383        assert!(report.contains(crate::HEMATITE_REPOSITORY_URL));
384    }
385}