use crate::AppState;
use anyhow::{anyhow, Context, Result};
use serde_json::{json, Value};
use trusty_common::memory_core::palace::PalaceId;
use trusty_common::memory_core::store::kg::Triple;
use super::helpers::{open_palace_handle, resolve_palace};
pub(crate) async fn handle_kg_assert(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "kg_assert")?;
let palace = palace.as_str();
let subject = args
.get("subject")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("kg_assert: missing 'subject'"))?
.to_string();
let predicate = args
.get("predicate")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("kg_assert: missing 'predicate'"))?
.to_string();
let object = args
.get("object")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("kg_assert: missing 'object'"))?
.to_string();
let confidence = args
.get("confidence")
.and_then(|v| v.as_f64())
.map(|c| (c as f32).clamp(0.0, 1.0))
.unwrap_or(1.0);
let provenance = args
.get("provenance")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let handle = open_palace_handle(state, palace)?;
let triple = Triple {
subject,
predicate,
object,
valid_from: chrono::Utc::now(),
valid_to: None,
confidence,
provenance,
};
let is_hot = crate::prompt_facts::is_hot_predicate(&triple.predicate);
handle.kg.assert(triple).await.context("kg.assert")?;
if is_hot {
if let Err(e) = crate::prompt_facts::rebuild_prompt_cache(state).await {
tracing::warn!("rebuild_prompt_cache after kg_assert failed: {e:#}");
}
}
Ok(json!({ "status": "asserted" }))
}
pub(crate) async fn handle_add_alias(state: &AppState, args: Value) -> Result<Value> {
let short = args
.get("short")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("add_alias: missing 'short'"))?
.to_string();
let full = args
.get("full")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("add_alias: missing 'full'"))?
.to_string();
let extra = args
.get("extra")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let palace = resolve_palace(state, &args, "add_alias")?;
let handle = open_palace_handle(state, &palace)?;
let object = match extra.as_deref() {
Some(e) if !e.is_empty() => format!("{full} ({e})"),
_ => full.clone(),
};
let triple = Triple {
subject: short.clone(),
predicate: "is_alias_for".to_string(),
object,
valid_from: chrono::Utc::now(),
valid_to: None,
confidence: 1.0,
provenance: Some("add_alias".to_string()),
};
handle
.kg
.assert(triple)
.await
.context("kg.assert (alias)")?;
if let Err(e) = crate::prompt_facts::rebuild_prompt_cache(state).await {
tracing::warn!("rebuild_prompt_cache after add_alias failed: {e:#}");
}
Ok(json!({ "asserted": true, "short": short, "full": full }))
}
pub(crate) async fn handle_list_prompt_facts(state: &AppState, _args: Value) -> Result<Value> {
let triples = crate::prompt_facts::gather_hot_triples(state).await?;
let payload: Vec<Value> = triples
.into_iter()
.map(|(subject, predicate, object)| {
json!({ "subject": subject, "predicate": predicate, "object": object })
})
.collect();
Ok(json!({ "facts": payload }))
}
pub(crate) async fn handle_remove_prompt_fact(state: &AppState, args: Value) -> Result<Value> {
let subject = args
.get("subject")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("remove_prompt_fact: missing 'subject'"))?
.to_string();
let predicate = args
.get("predicate")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("remove_prompt_fact: missing 'predicate'"))?
.to_string();
let mut closed_total: usize = 0;
for palace_id in state.registry.list() {
if let Some(handle) = state.registry.get(&palace_id) {
match handle.kg.retract(&subject, &predicate).await {
Ok(n) => closed_total += n,
Err(e) => tracing::warn!(
palace = %palace_id.as_str(),
"retract failed: {e:#}",
),
}
}
}
if closed_total > 0 {
if let Err(e) = crate::prompt_facts::rebuild_prompt_cache(state).await {
tracing::warn!("rebuild_prompt_cache after remove_prompt_fact failed: {e:#}");
}
Ok(json!({ "removed": true, "closed": closed_total }))
} else {
Ok(json!({ "removed": false, "reason": "not found" }))
}
}
pub(crate) async fn handle_kg_query(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "kg_query")?;
let subject = args
.get("subject")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("kg_query: missing 'subject'"))?;
let handle = open_palace_handle(state, &palace)?;
let triples = handle
.kg
.query_active(subject)
.await
.context("kg.query_active")?;
let payload: Vec<Value> = triples
.iter()
.map(|t| {
json!({
"subject": t.subject,
"predicate": t.predicate,
"object": t.object,
"valid_from": t.valid_from.to_rfc3339(),
"valid_to": t.valid_to.as_ref().map(|d| d.to_rfc3339()),
"confidence": t.confidence,
"provenance": t.provenance,
})
})
.collect();
let mut response = json!({ "subject": subject, "triples": payload });
if crate::bootstrap::is_kg_empty_for_subject(&triples) {
response["hint"] = Value::String(crate::bootstrap::KG_EMPTY_HINT.to_string());
}
Ok(response)
}
pub(crate) async fn handle_kg_gaps(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "kg_gaps")?;
let _handle = open_palace_handle(state, &palace)?;
let pid = PalaceId::new(&palace);
let cached = state.registry.get_gaps(&pid).unwrap_or_default();
let payload: Vec<Value> = cached
.into_iter()
.map(|g| {
json!({
"entities": g.entities,
"internal_density": g.internal_density,
"external_bridges": g.external_bridges,
"suggested_exploration": g.suggested_exploration,
})
})
.collect();
Ok(json!({ "palace": palace, "gaps": payload }))
}
pub(crate) async fn handle_get_prompt_context(state: &AppState, args: Value) -> Result<Value> {
let query = args
.get("query")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let cache_snapshot = {
let guard = state.prompt_context_cache.read().await;
guard.clone()
};
let body = if let Some(q) = query.as_deref() {
let needle = q.to_lowercase();
let filtered: Vec<(String, String, String)> = cache_snapshot
.triples
.into_iter()
.filter(|(subject, _predicate, object)| {
subject.to_lowercase().contains(&needle) || object.to_lowercase().contains(&needle)
})
.collect();
let formatted = crate::prompt_facts::build_prompt_context(&filtered);
if formatted.is_empty() {
"No project context found matching your query.".to_string()
} else {
formatted
}
} else if cache_snapshot.formatted.is_empty() {
"No prompt facts stored yet.".to_string()
} else {
cache_snapshot.formatted
};
Ok(Value::String(body))
}
pub(crate) async fn handle_discover_aliases(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "discover_aliases")?;
let project_root = args
.get("project_root")
.and_then(|v| v.as_str())
.map(std::path::PathBuf::from)
.or_else(|| std::env::current_dir().ok())
.ok_or_else(|| anyhow!("discover_aliases: no project_root and cwd unavailable"))?;
let discoveries = crate::discovery::discover_project_aliases(&project_root).await?;
let handle = open_palace_handle(state, &palace)?;
let mut already_known = 0usize;
let mut newly_asserted = 0usize;
let mut reported: Vec<Value> = Vec::with_capacity(discoveries.len());
for d in &discoveries {
let active = handle
.kg
.query_active(&d.short)
.await
.context("kg.query_active")?;
let exists = active
.iter()
.any(|t| t.predicate == "is_alias_for" && t.object == d.full);
if exists {
already_known += 1;
continue;
}
let triple = Triple {
subject: d.short.clone(),
predicate: "is_alias_for".to_string(),
object: d.full.clone(),
valid_from: chrono::Utc::now(),
valid_to: None,
confidence: 1.0,
provenance: Some(format!("discover_aliases:{}", d.source.as_str())),
};
handle
.kg
.assert(triple)
.await
.context("kg.assert (discover)")?;
newly_asserted += 1;
reported.push(json!({
"short": d.short,
"full": d.full,
"source": d.source.as_str(),
}));
}
if newly_asserted > 0 {
if let Err(e) = crate::prompt_facts::rebuild_prompt_cache(state).await {
tracing::warn!("rebuild_prompt_cache after discover_aliases failed: {e:#}");
}
}
Ok(json!({
"discovered": reported,
"already_known": already_known,
"new": newly_asserted,
"palace": palace,
}))
}
pub(crate) async fn handle_kg_bootstrap(state: &AppState, args: Value) -> Result<Value> {
let palace = resolve_palace(state, &args, "kg_bootstrap")?;
let project_path = args
.get("project_path")
.and_then(|v| v.as_str())
.map(std::path::PathBuf::from);
let result = crate::bootstrap::bootstrap_palace(state, &palace, project_path.as_deref())
.await
.context("bootstrap_palace")?;
if let Err(e) = crate::prompt_facts::rebuild_prompt_cache(state).await {
tracing::warn!("rebuild_prompt_cache after kg_bootstrap failed: {e:#}");
}
crate::bootstrap::result_to_json(&result)
}
pub(crate) async fn handle_upgrade_tool(state: &AppState, args: Value) -> Result<Value> {
let check = args.get("check").and_then(Value::as_bool).unwrap_or(true);
let confirm = args
.get("confirm")
.and_then(Value::as_bool)
.unwrap_or(false);
let crate_name = env!("CARGO_PKG_NAME");
let current = env!("CARGO_PKG_VERSION");
let info = trusty_common::update::check_crates_io(crate_name, current).await;
let (latest, is_update) = match &info {
Some(u) => (u.latest.as_str(), true),
None => (current, false),
};
if check || !confirm {
let msg = if is_update {
format!(
"Update available: {crate_name} {latest} (you have {current}). \
Call with confirm=true to install."
)
} else {
format!("{crate_name} {current} is already up to date.")
};
return Ok(
serde_json::json!({ "status": "checked", "current": current, "latest": latest, "update_available": is_update, "message": msg }),
);
}
if !is_update {
return Ok(serde_json::json!({
"status": "up_to_date",
"current": current,
"message": format!("{crate_name} {current} is already up to date — nothing to install.")
}));
}
let upgrade_state = state.update_available.clone();
let latest_owned = latest.to_string();
let crate_name_owned = crate_name.to_string();
let response = serde_json::json!({
"status": "installing",
"current": current,
"latest": latest_owned,
"message": format!(
"Installing {crate_name} {latest_owned} — daemon will restart automatically \
under launchd, or you will be prompted to restart manually."
)
});
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
match trusty_common::update::upgrade_and_restart(&crate_name_owned, &crate_name_owned).await
{
Ok(Some(hint)) => {
tracing::info!("{hint}");
eprintln!("{hint}");
}
Ok(None) => {}
Err(e) => {
tracing::error!("upgrade_and_restart failed: {e:#}");
eprintln!("[trusty-memory] upgrade failed: {e:#}");
if let Ok(mut g) = upgrade_state.lock() {
*g = None;
}
}
}
});
Ok(response)
}