1pub mod agent;
2
3#[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
95pub 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}