use std::path::PathBuf;
use clap::{Parser, Subcommand};
use serde_json::json;
pub use crate::backup::BackupCommands;
pub use crate::daemon::DaemonCommands;
pub use crate::hook::HookCommands;
use crate::{RecallParams, RecordParams};
fn default_db() -> PathBuf {
crate::paths::default_db_path()
}
#[derive(Parser)]
#[command(name = "innate", version, about = "Self-growing knowledge layer")]
pub struct Cli {
#[arg(long, global = true, env = "INNATE_DB")]
pub db: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Recall {
query: String,
#[arg(long, default_value = "6000")]
budget: usize,
#[arg(long)]
top: Option<usize>,
#[arg(long, default_value = "text")]
format: String,
#[arg(long)]
include_sparks: bool,
#[arg(long, default_value = "false")]
expand_deps: String,
#[arg(long)]
allow_trim: bool,
#[arg(long, default_value = "off")]
refine_mode: String,
#[arg(long, default_value = "cli")]
source: String,
},
Record {
trace_id: String,
#[arg(long)]
query: Option<String>,
#[arg(long)]
outcome: Option<String>,
#[arg(long)]
used: Option<String>,
#[arg(long, default_value = "explicit")]
used_attribution: String,
#[arg(long)]
used_partial: bool,
#[arg(long)]
output: Option<String>,
#[arg(long)]
output_summary: Option<String>,
#[arg(long)]
nomination: Option<String>,
#[arg(long, default_value = "cli")]
source: String,
#[arg(long)]
feedback: Option<String>,
#[arg(long, default_value = "user")]
feedback_kind: String,
#[arg(long)]
feedback_actor: Option<String>,
#[arg(long)]
feedback_reason: Option<String>,
#[arg(long)]
task_state: Option<String>,
#[arg(long, default_value = "0")]
priority: i64,
},
Add {
content: String,
#[arg(long, default_value = "note")]
kind: String,
#[arg(long)]
trigger: Option<String>,
#[arg(long)]
anti_trigger: Option<String>,
#[arg(long, default_value = "chat")]
source: String,
#[arg(long)]
skill_name: Option<String>,
#[arg(long = "depends-on")]
depends_on: Vec<String>,
#[arg(long, default_value = "hard")]
dep_kind: String,
},
Spark {
content: String,
#[arg(long)]
trigger: Option<String>,
},
Evolve {
#[arg(long, default_value = "manual")]
trigger: String,
#[arg(long)]
rebuild_embeddings: bool,
},
Inspect { id: Option<String> },
Approve { chunk_id: String },
Archive {
chunk_id: String,
#[arg(long, default_value = "stale")]
reason: String,
},
Invalidate {
chunk_id: String,
#[arg(long, default_value = "")]
reason: String,
},
Restore { chunk_id: String },
MatureSpark { spark_id: String, to: String },
PromoteSpark {
spark_id: String,
#[arg(long, default_value = "note")]
to: String,
},
DropSpark {
spark_id: String,
#[arg(long, default_value = "")]
reason: String,
},
Backup {
#[command(subcommand)]
action: BackupCommands,
},
Install,
Uninstall {
#[arg(long, short = 'y')]
yes: bool,
#[arg(long)]
purge_data: bool,
},
Migrate,
Vacuum,
Upgrade {
#[arg(long, value_name = "VERSION")]
version: Option<String>,
#[arg(long)]
check: bool,
},
Daemon {
#[command(subcommand)]
action: DaemonCommands,
},
Mcp,
Web {
#[arg(long, default_value = "127.0.0.1")]
bind: String,
#[arg(long, default_value_t = 8788)]
port: u16,
#[arg(long)]
no_token: bool,
#[arg(long)]
allow_remote: bool,
},
Hook {
#[command(subcommand)]
action: HookCommands,
},
}
pub fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
crate::paths::ensure_layout();
let db_path = cli.db.unwrap_or_else(default_db);
if let Commands::Mcp = &cli.command {
return crate::mcp::run_server(db_path);
}
if let Commands::Install = &cli.command {
return crate::install::run_install();
}
if let Commands::Uninstall { yes, purge_data } = &cli.command {
return crate::install::run_uninstall(*yes, *purge_data);
}
if let Commands::Migrate = &cli.command {
let applied = crate::migrate::run_migrations(&db_path)?;
if applied.is_empty() {
println!("already at 4.14 — nothing to do");
} else {
for step in &applied {
println!(" applied: {step}");
}
println!("migration complete");
}
return Ok(());
}
if let Commands::Daemon { action } = &cli.command {
return crate::daemon::run_command(action, &db_path);
}
if let Commands::Backup { action } = &cli.command {
return crate::backup::run_command(action, &db_path);
}
if let Commands::Upgrade { version, check } = &cli.command {
return crate::upgrade::run_upgrade(version.as_deref(), &db_path, *check);
}
if let Commands::Hook { action } = &cli.command {
return crate::hook::run_command(action);
}
let kb = crate::open_kb(&db_path)?;
match cli.command {
Commands::Recall {
query,
budget,
top,
format,
include_sparks,
expand_deps,
allow_trim,
refine_mode,
source,
} => {
let result = kb.recall(RecallParams {
query: &query,
budget,
trace: true,
include_sparks,
top,
source: &source,
expand_deps: &expand_deps,
allow_trim,
refine_mode: &refine_mode,
})?;
match format.as_str() {
"json" => println!(
"{}",
serde_json::to_string_pretty(&json!({
"trace_id": result.trace_id,
"knowledge": result.knowledge,
"sparks": result.sparks,
"empty": result.empty,
}))?
),
"prompt" => {
for chunk in &result.knowledge {
let content = chunk.get("content").and_then(|v| v.as_str()).unwrap_or("");
println!("{content}\n---");
}
println!("<!-- innate_trace_id: {} -->", result.trace_id);
println!(
"<!-- innate_selected: {} -->",
result
.knowledge
.iter()
.filter_map(|c| c.get("id").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.join(",")
);
}
_ => {
for chunk in &result.knowledge {
let id = chunk.get("id").and_then(|v| v.as_str()).unwrap_or("?");
let content = chunk.get("content").and_then(|v| v.as_str()).unwrap_or("");
let conf = chunk
.get("confidence")
.and_then(|v| v.as_f64())
.unwrap_or(0.5);
println!("[{id}] (conf={conf:.2})\n{content}\n");
}
if result.empty {
println!("(no results)");
}
}
}
}
Commands::Record {
trace_id,
query,
outcome,
used,
used_attribution,
used_partial,
output,
output_summary,
nomination,
source,
feedback,
feedback_kind,
feedback_actor,
feedback_reason,
task_state,
priority,
} => {
let used_ids = used.as_deref().map(|raw| {
raw.split(',')
.map(str::trim)
.filter(|id| !id.is_empty())
.map(str::to_string)
.collect::<Vec<_>>()
});
let used_ref = used_ids.as_deref();
let (fb_up, fb_down): (Option<Vec<String>>, Option<Vec<String>>) =
match feedback.as_deref() {
Some("up") if used_ids.as_ref().is_some_and(|ids| !ids.is_empty()) => {
(used_ids.clone(), None)
}
Some("down") if used_ids.as_ref().is_some_and(|ids| !ids.is_empty()) => {
(None, used_ids.clone())
}
Some("up") => (None, None), Some("down") => (None, None),
_ => (None, None),
};
let fb_up_ref = fb_up.as_deref();
let fb_down_ref = fb_down.as_deref();
kb.record(RecordParams {
trace_id: &trace_id,
query: query.as_deref(),
output: output.as_deref(),
output_summary: output_summary.as_deref(),
outcome: outcome.as_deref(),
used: used_ref,
used_attribution: &used_attribution,
used_complete: Some(!used_partial),
feedback_up: fb_up_ref,
feedback_down: fb_down_ref,
feedback_kind: &feedback_kind,
feedback_actor: feedback_actor.as_deref(),
feedback_reason: feedback_reason.as_deref(),
nomination: nomination.as_deref(),
priority,
task_state: task_state.as_deref(),
source: &source,
})?;
println!("recorded");
}
Commands::Add {
content,
kind,
trigger,
anti_trigger,
source,
skill_name,
depends_on,
dep_kind,
} => {
let content = if kind == "skill" {
let p = std::path::Path::new(&content);
if p.exists() && p.is_file() {
std::fs::read_to_string(p).map_err(|e| {
anyhow::anyhow!("Failed to read skill file {}: {e}", p.display())
})?
} else {
content
}
} else {
content
};
let deps: Vec<(String, String)> = depends_on
.iter()
.map(|d| (d.clone(), dep_kind.clone()))
.collect();
let id = kb.add_with_deps(
&content,
&kind,
trigger.as_deref(),
anti_trigger.as_deref(),
&source,
skill_name.as_deref(),
&deps,
)?;
println!("{id}");
}
Commands::Spark { content, trigger } => {
let id = kb.spark(&content, trigger.as_deref(), None)?;
println!("{id}");
}
Commands::Evolve {
trigger,
rebuild_embeddings,
} => {
if rebuild_embeddings {
let rebuilt = kb.rebuild_embeddings()?;
let report = kb.evolve(&trigger)?;
println!(
"{}",
serde_json::to_string_pretty(&json!({
"rebuilt_embeddings": rebuilt,
"evolve": report
}))?
);
} else {
let report = kb.evolve(&trigger)?;
println!("{}", serde_json::to_string_pretty(&report)?);
}
}
Commands::Inspect { id } => match id.as_deref() {
None => {
let info = kb.inspect()?;
println!("{}", serde_json::to_string_pretty(&info)?);
}
Some(id) => {
let detail = kb.inspect_id(id)?;
println!("{}", serde_json::to_string_pretty(&detail)?);
}
},
Commands::Approve { chunk_id } => {
kb.approve(&chunk_id)?;
println!("approved");
}
Commands::Archive { chunk_id, reason } => {
kb.archive(&chunk_id, &reason)?;
println!("archived");
}
Commands::Invalidate { chunk_id, reason } => {
kb.invalidate(&chunk_id, &reason)?;
println!("invalidated");
}
Commands::Restore { chunk_id } => {
kb.restore(&chunk_id)?;
println!("restored");
}
Commands::MatureSpark { spark_id, to } => {
kb.mature_spark(&spark_id, &to)?;
println!("matured");
}
Commands::PromoteSpark { spark_id, to } => {
let id = kb.promote_spark(&spark_id, &to)?;
println!("{id}");
}
Commands::DropSpark { spark_id, reason } => {
kb.drop_spark(&spark_id, &reason)?;
println!("dropped");
}
Commands::Vacuum => {
let (before, after) = kb.storage.vacuum()?;
let mb = |b: i64| b as f64 / 1_048_576.0;
println!(
"vacuumed: {:.2} MB → {:.2} MB (reclaimed {:.2} MB)",
mb(before),
mb(after),
mb(before - after)
);
}
Commands::Web {
bind,
port,
no_token,
allow_remote,
} => {
let loopback = crate::web::is_loopback(&bind);
if !loopback && !allow_remote {
anyhow::bail!(
"refusing to bind non-loopback address {bind} without --allow-remote \
(this exposes the knowledge base to the network)"
);
}
if !loopback && no_token {
anyhow::bail!(
"--no-token cannot be combined with a non-loopback bind: a network-exposed \
server must keep the auth token to gate reads and writes"
);
}
crate::web::serve(kb, &bind, port, !no_token)?;
}
Commands::Mcp
| Commands::Install
| Commands::Uninstall { .. }
| Commands::Migrate
| Commands::Upgrade { .. }
| Commands::Daemon { .. }
| Commands::Backup { .. }
| Commands::Hook { .. } => unreachable!(),
}
Ok(())
}