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(
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 #[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 #[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}