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 and workstation assistant for real developer workflows.";
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 coding harness: the TUI, tool use, file editing, workflow control, voice integration, and workstation-assistant architecture.\n\nRepo: {}",
82 HEMATITE_AUTHOR, HEMATITE_SHORT_DESCRIPTION, HEMATITE_REPOSITORY_URL
83 )
84}
85
86pub 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 - Local-first AI coding harness and workstation assistant",
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 help = "After generating a --report or --diagnose, open the saved file in the default application (browser for HTML, editor for Markdown)"
209 )]
210 pub open: bool,
211
212 #[arg(long, hide = true)]
213 pub pdf_extract_helper: Option<String>,
214
215 #[arg(long, hide = true)]
216 pub teleported_from: Option<String>,
217}
218
219#[cfg(test)]
220mod tests {
221 #[test]
222 fn version_report_contains_release_version() {
223 let report = crate::hematite_version_report();
224 assert!(report.contains(crate::HEMATITE_VERSION));
225 assert!(report.contains("Build:"));
226 }
227
228 #[test]
229 fn about_report_contains_author_and_repo() {
230 let report = crate::hematite_about_report();
231 assert!(report.contains(crate::HEMATITE_AUTHOR));
232 assert!(report.contains(crate::HEMATITE_REPOSITORY_URL));
233 }
234}