1pub mod diff;
7mod extractors;
8pub mod triggers;
9
10use codemem_core::{CodememError, MemoryType, RelationshipType};
11use serde::Deserialize;
12use std::collections::HashMap;
13
14pub use triggers::{check_triggers, AutoInsight};
15
16use extractors::{
17 extract_agent_communication, extract_bash, extract_edit, extract_glob, extract_grep,
18 extract_list_dir, extract_read, extract_web, extract_write,
19};
20
21const MAX_CONTENT_SIZE: usize = 100 * 1024;
23
24#[derive(Debug, Deserialize)]
30pub struct HookPayload {
31 pub tool_name: String,
32 pub tool_input: serde_json::Value,
33 pub tool_response: serde_json::Value,
34 pub session_id: Option<String>,
35 pub cwd: Option<String>,
36 pub hook_event_name: Option<String>,
38 pub transcript_path: Option<String>,
40 pub permission_mode: Option<String>,
42 pub tool_use_id: Option<String>,
44}
45
46impl HookPayload {
47 pub fn tool_response_text(&self) -> String {
59 match &self.tool_response {
60 serde_json::Value::String(s) => s.clone(),
61 serde_json::Value::Null => String::new(),
62 serde_json::Value::Object(obj) => {
63 if let Some(content) = obj
65 .get("file")
66 .and_then(|f| f.get("content"))
67 .and_then(|c| c.as_str())
68 {
69 return content.to_string();
70 }
71 if let Some(text) = obj.get("text").and_then(|t| t.as_str()) {
73 return text.to_string();
74 }
75 if let Some(stdout) = obj.get("stdout").and_then(|s| s.as_str()) {
77 return stdout.to_string();
78 }
79 serde_json::to_string(&self.tool_response).unwrap_or_default()
81 }
82 other => other.to_string(),
83 }
84 }
85}
86
87#[derive(Debug)]
89pub struct ExtractedMemory {
90 pub content: String,
91 pub memory_type: MemoryType,
92 pub tags: Vec<String>,
93 pub metadata: HashMap<String, serde_json::Value>,
94 pub graph_node: Option<codemem_core::GraphNode>,
95 pub graph_edges: Vec<PendingEdge>,
96 pub session_id: Option<String>,
97}
98
99#[derive(Debug)]
101pub struct PendingEdge {
102 pub src_id: String,
103 pub dst_id: String,
104 pub relationship: RelationshipType,
105}
106
107pub fn parse_payload(json: &str) -> Result<HookPayload, CodememError> {
109 serde_json::from_str(json)
110 .map_err(|e| CodememError::Hook(format!("Failed to parse payload: {e}")))
111}
112
113pub fn extract(payload: &HookPayload) -> Result<Option<ExtractedMemory>, CodememError> {
115 let response_text = payload.tool_response_text();
119 if response_text.len() > MAX_CONTENT_SIZE {
120 tracing::debug!("Skipping large response ({} bytes)", response_text.len());
121 return Ok(None);
122 }
123
124 match payload.tool_name.as_str() {
125 "Read" => extract_read(payload, &response_text),
126 "Glob" => extract_glob(payload, &response_text),
127 "Grep" => extract_grep(payload, &response_text),
128 "Edit" | "MultiEdit" => extract_edit(payload),
129 "Write" => extract_write(payload, &response_text),
130 "Bash" => extract_bash(payload, &response_text),
131 "WebFetch" | "WebSearch" => extract_web(payload, &response_text),
132 "Agent" | "SendMessage" => extract_agent_communication(payload, &response_text),
133 "ListFiles" | "ListDir" => extract_list_dir(payload, &response_text),
134 _ => {
135 tracing::debug!("Unknown tool: {}", payload.tool_name);
136 Ok(None)
137 }
138 }
139}
140
141pub fn resolve_edges(
149 extracted: &mut ExtractedMemory,
150 existing_node_ids: &std::collections::HashSet<String>,
151) {
152 let current_node_id = match &extracted.graph_node {
154 Some(node) => node.id.clone(),
155 None => return,
156 };
157
158 let tool = extracted
160 .metadata
161 .get("tool")
162 .and_then(|v| v.as_str())
163 .unwrap_or("");
164
165 match tool {
169 "Edit" | "Write" => {
170 if existing_node_ids.contains(¤t_node_id) {
171 extracted.graph_edges.push(PendingEdge {
172 src_id: current_node_id,
173 dst_id: String::new(), relationship: RelationshipType::EvolvedInto,
175 });
176 }
177 }
178 _ => {}
179 }
180}
181
182pub fn materialize_edges(pending: &[PendingEdge], memory_id: &str) -> Vec<codemem_core::Edge> {
188 let now = chrono::Utc::now();
189 pending
190 .iter()
191 .map(|pe| {
192 if pe.dst_id.is_empty() {
194 let edge_id = format!("{}-{}-{}", pe.src_id, pe.relationship, memory_id);
198 let mut props = HashMap::new();
199 props.insert(
200 "triggered_by".to_string(),
201 serde_json::Value::String(memory_id.to_string()),
202 );
203 codemem_core::Edge {
204 id: edge_id,
205 src: pe.src_id.clone(),
206 dst: pe.src_id.clone(),
207 relationship: pe.relationship,
208 weight: 1.0,
209 properties: props,
210 created_at: now,
211 valid_from: None,
212 valid_to: None,
213 }
214 } else {
215 let edge_id = format!("{}-{}-{}", pe.src_id, pe.relationship, pe.dst_id);
216 codemem_core::Edge {
217 id: edge_id,
218 src: pe.src_id.clone(),
219 dst: pe.dst_id.clone(),
220 relationship: pe.relationship,
221 weight: 1.0,
222 properties: HashMap::new(),
223 created_at: now,
224 valid_from: None,
225 valid_to: None,
226 }
227 }
228 })
229 .collect()
230}
231
232pub use codemem_core::content_hash;
234
235#[cfg(test)]
236#[path = "tests/lib_tests.rs"]
237mod tests;
238
239#[cfg(test)]
240#[path = "tests/hooks_integration.rs"]
241mod hooks_integration_tests;