1use std::path::PathBuf;
4
5use clap::{Parser, Subcommand};
6use serde_json::json;
7
8pub use crate::backup::BackupCommands;
9pub use crate::daemon::DaemonCommands;
10pub use crate::hook::HookCommands;
11
12fn default_db() -> PathBuf {
13 crate::paths::default_db_path()
14}
15
16#[derive(Parser)]
17#[command(name = "innate", version, about = "Self-growing knowledge layer")]
18pub struct Cli {
19 #[arg(long, global = true, env = "INNATE_DB")]
20 pub db: Option<PathBuf>,
21
22 #[command(subcommand)]
23 pub command: Commands,
24}
25
26#[derive(Subcommand)]
27pub enum Commands {
28 Recall {
30 query: String,
31 #[arg(long, default_value = "6000")]
32 budget: usize,
33 #[arg(long)]
34 top: Option<usize>,
35 #[arg(long, default_value = "text")]
36 format: String,
37 #[arg(long)]
38 include_sparks: bool,
39 #[arg(long, default_value = "false")]
41 expand_deps: String,
42 #[arg(long)]
44 allow_trim: bool,
45 #[arg(long, default_value = "off")]
47 refine_mode: String,
48 #[arg(long, default_value = "cli")]
50 source: String,
51 },
52 Record {
54 trace_id: String,
55 #[arg(long)]
56 query: Option<String>,
57 #[arg(long)]
58 outcome: Option<String>,
59 #[arg(long)]
61 used: Option<String>,
62 #[arg(long, default_value = "explicit")]
63 used_attribution: String,
64 #[arg(long)]
66 used_partial: bool,
67 #[arg(long)]
68 output: Option<String>,
69 #[arg(long)]
70 output_summary: Option<String>,
71 #[arg(long)]
72 nomination: Option<String>,
73 #[arg(long, default_value = "cli")]
74 source: String,
75 #[arg(long)]
77 feedback: Option<String>,
78 #[arg(long, default_value = "user")]
79 feedback_kind: String,
80 #[arg(long)]
81 feedback_actor: Option<String>,
82 #[arg(long)]
83 feedback_reason: Option<String>,
84 #[arg(long)]
85 task_state: Option<String>,
86 #[arg(long, default_value = "0")]
87 priority: i64,
88 },
89 Add {
91 content: String,
92 #[arg(long, default_value = "note")]
93 kind: String,
94 #[arg(long)]
95 trigger: Option<String>,
96 #[arg(long)]
97 anti_trigger: Option<String>,
98 #[arg(long, default_value = "chat")]
99 source: String,
100 #[arg(long)]
101 skill_name: Option<String>,
102 },
103 Spark {
105 content: String,
106 #[arg(long)]
107 trigger: Option<String>,
108 },
109 Evolve {
111 #[arg(long, default_value = "manual")]
112 trigger: String,
113 #[arg(long)]
115 rebuild_embeddings: bool,
116 },
117 Inspect { id: Option<String> },
119 Approve { chunk_id: String },
121 Archive {
123 chunk_id: String,
124 #[arg(long, default_value = "stale")]
125 reason: String,
126 },
127 Invalidate {
129 chunk_id: String,
130 #[arg(long, default_value = "")]
131 reason: String,
132 },
133 Restore { chunk_id: String },
135 MatureSpark { spark_id: String, to: String },
137 PromoteSpark {
139 spark_id: String,
140 #[arg(long, default_value = "note")]
141 to: String,
142 },
143 DropSpark {
145 spark_id: String,
146 #[arg(long, default_value = "")]
147 reason: String,
148 },
149 Backup {
151 #[command(subcommand)]
152 action: BackupCommands,
153 },
154 Install,
156 Uninstall {
158 #[arg(long, short = 'y')]
160 yes: bool,
161 #[arg(long)]
163 purge_data: bool,
164 },
165 Migrate,
167 Vacuum,
169 Upgrade {
171 #[arg(long, value_name = "VERSION")]
173 version: Option<String>,
174 #[arg(long)]
176 check: bool,
177 },
178 Daemon {
180 #[command(subcommand)]
181 action: DaemonCommands,
182 },
183 Mcp,
185 Hook {
187 #[command(subcommand)]
188 action: HookCommands,
189 },
190}
191
192pub fn run() -> anyhow::Result<()> {
193 let cli = Cli::parse();
194 crate::paths::ensure_layout();
197 let db_path = cli.db.unwrap_or_else(default_db);
198
199 if let Commands::Mcp = &cli.command {
200 return crate::mcp::run_server(db_path);
201 }
202
203 if let Commands::Install = &cli.command {
204 return crate::install::run_install();
205 }
206
207 if let Commands::Uninstall { yes, purge_data } = &cli.command {
208 return crate::install::run_uninstall(*yes, *purge_data);
209 }
210
211 if let Commands::Migrate = &cli.command {
212 let applied = crate::migrate::run_migrations(&db_path)?;
213 if applied.is_empty() {
214 println!("already at 4.14 — nothing to do");
215 } else {
216 for step in &applied {
217 println!(" applied: {step}");
218 }
219 println!("migration complete");
220 }
221 return Ok(());
222 }
223
224 if let Commands::Daemon { action } = &cli.command {
225 return crate::daemon::run_command(action, &db_path);
226 }
227
228 if let Commands::Backup { action } = &cli.command {
229 return crate::backup::run_command(action, &db_path);
230 }
231
232 if let Commands::Upgrade { version, check } = &cli.command {
233 return crate::upgrade::run_upgrade(version.as_deref(), &db_path, *check);
234 }
235
236 if let Commands::Hook { action } = &cli.command {
237 return crate::hook::run_command(action);
238 }
239
240 let kb = crate::open_kb(&db_path)?;
241
242 match cli.command {
243 Commands::Recall {
244 query,
245 budget,
246 top,
247 format,
248 include_sparks,
249 expand_deps,
250 allow_trim,
251 refine_mode,
252 source,
253 } => {
254 let result = kb.recall(
255 &query,
256 budget,
257 true,
258 include_sparks,
259 top,
260 &source,
261 &expand_deps,
262 allow_trim,
263 &refine_mode,
264 )?;
265 match format.as_str() {
266 "json" => println!(
267 "{}",
268 serde_json::to_string_pretty(&json!({
269 "trace_id": result.trace_id,
270 "knowledge": result.knowledge,
271 "sparks": result.sparks,
272 "empty": result.empty,
273 }))?
274 ),
275 "prompt" => {
276 for chunk in &result.knowledge {
277 let content = chunk.get("content").and_then(|v| v.as_str()).unwrap_or("");
278 println!("{content}\n---");
279 }
280 println!("<!-- innate_trace_id: {} -->", result.trace_id);
282 println!(
283 "<!-- innate_selected: {} -->",
284 result
285 .knowledge
286 .iter()
287 .filter_map(|c| c.get("id").and_then(|v| v.as_str()))
288 .collect::<Vec<_>>()
289 .join(",")
290 );
291 }
292 _ => {
293 for chunk in &result.knowledge {
294 let id = chunk.get("id").and_then(|v| v.as_str()).unwrap_or("?");
295 let content = chunk.get("content").and_then(|v| v.as_str()).unwrap_or("");
296 let conf = chunk
297 .get("confidence")
298 .and_then(|v| v.as_f64())
299 .unwrap_or(0.5);
300 println!("[{id}] (conf={conf:.2})\n{content}\n");
301 }
302 if result.empty {
303 println!("(no results)");
304 }
305 }
306 }
307 }
308 Commands::Record {
309 trace_id,
310 query,
311 outcome,
312 used,
313 used_attribution,
314 used_partial,
315 output,
316 output_summary,
317 nomination,
318 source,
319 feedback,
320 feedback_kind,
321 feedback_actor,
322 feedback_reason,
323 task_state,
324 priority,
325 } => {
326 let used_ids = used.as_deref().map(|raw| {
327 raw.split(',')
328 .map(str::trim)
329 .filter(|id| !id.is_empty())
330 .map(str::to_string)
331 .collect::<Vec<_>>()
332 });
333 let used_ref = used_ids.as_deref();
334 let (fb_up, fb_down): (Option<Vec<String>>, Option<Vec<String>>) =
336 match feedback.as_deref() {
337 Some("up") if used_ids.as_ref().is_some_and(|ids| !ids.is_empty()) => {
338 (used_ids.clone(), None)
339 }
340 Some("down") if used_ids.as_ref().is_some_and(|ids| !ids.is_empty()) => {
341 (None, used_ids.clone())
342 }
343 Some("up") => (None, None), Some("down") => (None, None),
345 _ => (None, None),
346 };
347 let fb_up_ref = fb_up.as_deref();
348 let fb_down_ref = fb_down.as_deref();
349 kb.record_detailed(
350 &trace_id,
351 query.as_deref(),
352 output.as_deref(),
353 output_summary.as_deref(),
354 outcome.as_deref(),
355 used_ref,
356 &used_attribution,
357 !used_partial,
358 fb_up_ref,
359 fb_down_ref,
360 &feedback_kind,
361 feedback_actor.as_deref(),
362 feedback_reason.as_deref(),
363 nomination.as_deref(),
364 priority,
365 task_state.as_deref(),
366 &source,
367 )?;
368 println!("recorded");
369 }
370 Commands::Add {
371 content,
372 kind,
373 trigger,
374 anti_trigger,
375 source,
376 skill_name,
377 } => {
378 let content = if kind == "skill" {
380 let p = std::path::Path::new(&content);
381 if p.exists() && p.is_file() {
382 std::fs::read_to_string(p).map_err(|e| {
383 anyhow::anyhow!("Failed to read skill file {}: {e}", p.display())
384 })?
385 } else {
386 content
387 }
388 } else {
389 content
390 };
391 let id = kb.add(
392 &content,
393 &kind,
394 trigger.as_deref(),
395 anti_trigger.as_deref(),
396 &source,
397 skill_name.as_deref(),
398 )?;
399 println!("{id}");
400 }
401 Commands::Spark { content, trigger } => {
402 let id = kb.spark(&content, trigger.as_deref(), None)?;
403 println!("{id}");
404 }
405 Commands::Evolve {
406 trigger,
407 rebuild_embeddings,
408 } => {
409 if rebuild_embeddings {
410 let rebuilt = kb.rebuild_embeddings()?;
411 let report = kb.evolve(&trigger)?;
412 println!(
413 "{}",
414 serde_json::to_string_pretty(&json!({
415 "rebuilt_embeddings": rebuilt,
416 "evolve": report
417 }))?
418 );
419 } else {
420 let report = kb.evolve(&trigger)?;
421 println!("{}", serde_json::to_string_pretty(&report)?);
422 }
423 }
424 Commands::Inspect { id } => match id.as_deref() {
425 None => {
426 let info = kb.inspect()?;
427 println!("{}", serde_json::to_string_pretty(&info)?);
428 }
429 Some(id) => {
430 let detail = kb.inspect_id(id)?;
431 println!("{}", serde_json::to_string_pretty(&detail)?);
432 }
433 },
434 Commands::Approve { chunk_id } => {
435 kb.approve(&chunk_id)?;
436 println!("approved");
437 }
438 Commands::Archive { chunk_id, reason } => {
439 kb.archive(&chunk_id, &reason)?;
440 println!("archived");
441 }
442 Commands::Invalidate { chunk_id, reason } => {
443 kb.invalidate(&chunk_id, &reason)?;
444 println!("invalidated");
445 }
446 Commands::Restore { chunk_id } => {
447 kb.restore(&chunk_id)?;
448 println!("restored");
449 }
450 Commands::MatureSpark { spark_id, to } => {
451 kb.mature_spark(&spark_id, &to)?;
452 println!("matured");
453 }
454 Commands::PromoteSpark { spark_id, to } => {
455 let id = kb.promote_spark(&spark_id, &to)?;
456 println!("{id}");
457 }
458 Commands::DropSpark { spark_id, reason } => {
459 kb.drop_spark(&spark_id, &reason)?;
460 println!("dropped");
461 }
462 Commands::Vacuum => {
463 let (before, after) = kb.storage.vacuum()?;
464 let mb = |b: i64| b as f64 / 1_048_576.0;
465 println!(
466 "vacuumed: {:.2} MB → {:.2} MB (reclaimed {:.2} MB)",
467 mb(before),
468 mb(after),
469 mb(before - after)
470 );
471 }
472 Commands::Mcp
473 | Commands::Install
474 | Commands::Uninstall { .. }
475 | Commands::Migrate
476 | Commands::Upgrade { .. }
477 | Commands::Daemon { .. }
478 | Commands::Backup { .. }
479 | Commands::Hook { .. } => unreachable!(),
480 }
481 Ok(())
482}