pub mod diff;
mod extractors;
pub mod triggers;
use codemem_core::{CodememError, MemoryType, RelationshipType};
use serde::Deserialize;
use std::collections::HashMap;
pub use triggers::{check_triggers, AutoInsight};
use extractors::{
extract_agent_communication, extract_bash, extract_edit, extract_glob, extract_grep,
extract_list_dir, extract_read, extract_web, extract_write,
};
const MAX_CONTENT_SIZE: usize = 100 * 1024;
#[derive(Debug, Deserialize)]
pub struct HookPayload {
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_response: serde_json::Value,
pub session_id: Option<String>,
pub cwd: Option<String>,
pub hook_event_name: Option<String>,
pub transcript_path: Option<String>,
pub permission_mode: Option<String>,
pub tool_use_id: Option<String>,
}
impl HookPayload {
pub fn tool_response_text(&self) -> String {
match &self.tool_response {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => String::new(),
serde_json::Value::Object(obj) => {
if let Some(content) = obj
.get("file")
.and_then(|f| f.get("content"))
.and_then(|c| c.as_str())
{
return content.to_string();
}
if let Some(text) = obj.get("text").and_then(|t| t.as_str()) {
return text.to_string();
}
if let Some(stdout) = obj.get("stdout").and_then(|s| s.as_str()) {
return stdout.to_string();
}
serde_json::to_string(&self.tool_response).unwrap_or_default()
}
other => other.to_string(),
}
}
}
#[derive(Debug)]
pub struct ExtractedMemory {
pub content: String,
pub memory_type: MemoryType,
pub tags: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
pub graph_node: Option<codemem_core::GraphNode>,
pub graph_edges: Vec<PendingEdge>,
pub session_id: Option<String>,
}
#[derive(Debug)]
pub struct PendingEdge {
pub src_id: String,
pub dst_id: String,
pub relationship: RelationshipType,
}
pub fn parse_payload(json: &str) -> Result<HookPayload, CodememError> {
serde_json::from_str(json)
.map_err(|e| CodememError::Hook(format!("Failed to parse payload: {e}")))
}
pub fn extract(payload: &HookPayload) -> Result<Option<ExtractedMemory>, CodememError> {
let response_text = payload.tool_response_text();
if response_text.len() > MAX_CONTENT_SIZE {
tracing::debug!("Skipping large response ({} bytes)", response_text.len());
return Ok(None);
}
match payload.tool_name.as_str() {
"Read" => extract_read(payload, &response_text),
"Glob" => extract_glob(payload, &response_text),
"Grep" => extract_grep(payload, &response_text),
"Edit" | "MultiEdit" => extract_edit(payload),
"Write" => extract_write(payload, &response_text),
"Bash" => extract_bash(payload, &response_text),
"WebFetch" | "WebSearch" => extract_web(payload, &response_text),
"Agent" | "SendMessage" => extract_agent_communication(payload, &response_text),
"ListFiles" | "ListDir" => extract_list_dir(payload, &response_text),
_ => {
tracing::debug!("Unknown tool: {}", payload.tool_name);
Ok(None)
}
}
}
pub fn resolve_edges(
extracted: &mut ExtractedMemory,
existing_node_ids: &std::collections::HashSet<String>,
) {
let current_node_id = match &extracted.graph_node {
Some(node) => node.id.clone(),
None => return,
};
let tool = extracted
.metadata
.get("tool")
.and_then(|v| v.as_str())
.unwrap_or("");
match tool {
"Edit" | "Write" => {
if existing_node_ids.contains(¤t_node_id) {
extracted.graph_edges.push(PendingEdge {
src_id: current_node_id,
dst_id: String::new(), relationship: RelationshipType::EvolvedInto,
});
}
}
_ => {}
}
}
pub fn materialize_edges(pending: &[PendingEdge], memory_id: &str) -> Vec<codemem_core::Edge> {
let now = chrono::Utc::now();
pending
.iter()
.map(|pe| {
if pe.dst_id.is_empty() {
let edge_id = format!("{}-{}-{}", pe.src_id, pe.relationship, memory_id);
let mut props = HashMap::new();
props.insert(
"triggered_by".to_string(),
serde_json::Value::String(memory_id.to_string()),
);
codemem_core::Edge {
id: edge_id,
src: pe.src_id.clone(),
dst: pe.src_id.clone(),
relationship: pe.relationship,
weight: 1.0,
properties: props,
created_at: now,
valid_from: None,
valid_to: None,
}
} else {
let edge_id = format!("{}-{}-{}", pe.src_id, pe.relationship, pe.dst_id);
codemem_core::Edge {
id: edge_id,
src: pe.src_id.clone(),
dst: pe.dst_id.clone(),
relationship: pe.relationship,
weight: 1.0,
properties: HashMap::new(),
created_at: now,
valid_from: None,
valid_to: None,
}
}
})
.collect()
}
pub use codemem_core::content_hash;
#[cfg(test)]
#[path = "tests/lib_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests/hooks_integration.rs"]
mod hooks_integration_tests;