1use scone_core::{Engine, RecallOpts, auth};
11
12pub fn session_start(engine: &mut Engine, space_name: &str) -> String {
13 let mut run = || -> Result<String, String> {
14 let space = auth::resolve(engine, space_name, true).map_err(|e| e.to_string())?;
15 let profile = engine.profile(&space, 6).map_err(|e| e.to_string())?;
16 let mut out = String::new();
17 if !profile.static_facts.is_empty() {
18 out.push_str("Persistent memory for this project:\n");
19 for f in &profile.static_facts {
20 out.push_str(&format!("- {} {} {}\n", f.subject, f.predicate, f.object));
21 }
22 }
23 if !profile.dynamic.is_empty() {
24 out.push_str("Recent activity:\n");
25 for d in profile.dynamic.iter().take(3) {
26 out.push_str(&format!("- {}\n", d.replace('\n', " ")));
27 }
28 }
29 Ok(out)
30 };
31 run().unwrap_or_else(|e| {
32 eprintln!("scone hook session-start: {e}");
33 String::new()
34 })
35}
36
37pub fn user_prompt(engine: &mut Engine, space_name: &str, stdin: &str) -> String {
38 let mut run = || -> Result<String, String> {
39 let value: serde_json::Value = serde_json::from_str(stdin).map_err(|e| e.to_string())?;
40 let prompt = value["prompt"].as_str().ok_or("no prompt field")?;
41 if prompt.trim().len() < 8 {
42 return Ok(String::new());
43 }
44 let space = auth::resolve(engine, space_name, true).map_err(|e| e.to_string())?;
45 let pack = engine
46 .recall(
47 &space,
48 prompt,
49 &RecallOpts {
50 limit: 3,
51 ..Default::default()
52 },
53 )
54 .map_err(|e| e.to_string())?;
55 if pack.facts.is_empty() && pack.items.is_empty() {
56 return Ok(String::new());
57 }
58 let mut out = String::from("Relevant memory:\n");
59 for f in &pack.facts {
60 out.push_str(&format!("- {} {} {}\n", f.subject, f.predicate, f.object));
61 }
62 for item in &pack.items {
63 let text: String = item.text.chars().take(300).collect();
64 out.push_str(&format!("- [{}] {}\n", item.day(), text.replace('\n', " ")));
65 }
66 Ok(out)
67 };
68 run().unwrap_or_else(|e| {
69 eprintln!("scone hook user-prompt: {e}");
70 String::new()
71 })
72}
73
74pub fn session_end(engine: &mut Engine, space_name: &str, stdin: &str) {
75 let mut run = || -> Result<(), String> {
76 let value: serde_json::Value = serde_json::from_str(stdin).map_err(|e| e.to_string())?;
77 let path = value["transcript_path"]
78 .as_str()
79 .ok_or("no transcript_path")?;
80 let raw = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
81 let mut lines = Vec::new();
82 for line in raw.lines() {
83 let Ok(entry) = serde_json::from_str::<serde_json::Value>(line) else {
84 continue;
85 };
86 let role = entry["message"]["role"].as_str().unwrap_or_default();
87 if role != "user" && role != "assistant" {
88 continue;
89 }
90 let Some(parts) = entry["message"]["content"].as_array() else {
91 continue;
92 };
93 for part in parts {
94 if let Some(text) = part["text"].as_str()
95 && !text.trim().is_empty()
96 {
97 lines.push(format!("{role}: {text}"));
98 }
99 }
100 }
101 if lines.is_empty() {
102 return Ok(());
103 }
104 let space = auth::resolve(engine, space_name, true).map_err(|e| e.to_string())?;
105 let (episode_id, _fresh) = engine
106 .import_episode(&space, "conversation", &lines.join("\n"), None, None)
107 .map_err(|e| e.to_string())?;
108 engine
109 .tag_episode(&space, episode_id, &["claude-code"])
110 .map_err(|e| e.to_string())?;
111 if engine.has_llm() {
112 let _ = engine.distill(&space, 25);
113 }
114 Ok(())
115 };
116 if let Err(e) = run() {
117 eprintln!("scone hook session-end: {e}");
118 }
119}