Skip to main content

hematite/
lib.rs

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