lean_ctx/core/context_package/
export.rs1use std::path::{Path, PathBuf};
4
5use chrono::Utc;
6
7use super::bundle::{ContextPackage, KnowledgeFact, PackageMetadata, SessionSlice, FORMAT_VERSION};
8use crate::core::session::SessionState;
9use crate::core::session_summary;
10
11pub fn save_package(
15 session: &SessionState,
16 project_root: &str,
17 agent_id: Option<&str>,
18 description: Option<&str>,
19 output_path: Option<&Path>,
20) -> Result<PathBuf, String> {
21 let pkg = build_package(session, project_root, agent_id, description);
22 let json = serde_json::to_string_pretty(&pkg).map_err(|e| e.to_string())?;
23
24 let path = output_path.map_or_else(|| default_path(project_root, &session.id), PathBuf::from);
25
26 if let Some(parent) = path.parent() {
27 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
28 }
29 std::fs::write(&path, json).map_err(|e| e.to_string())?;
30 Ok(path)
31}
32
33fn build_package(
34 session: &SessionState,
35 project_root: &str,
36 agent_id: Option<&str>,
37 description: Option<&str>,
38) -> ContextPackage {
39 let summaries = session_summary::list(project_root);
40 let knowledge = load_knowledge_facts(project_root);
41
42 ContextPackage {
43 format_version: FORMAT_VERSION,
44 created_at: Utc::now(),
45 project_root: project_root.to_string(),
46 session_id: session.id.clone(),
47 metadata: PackageMetadata {
48 agent_id: agent_id.map(str::to_string),
49 description: description.map(str::to_string),
50 tool_calls: session.stats.total_tool_calls,
51 tokens_saved: session.stats.total_tokens_saved,
52 },
53 session: SessionSlice {
54 task: session.task.clone(),
55 findings: session.findings.clone(),
56 decisions: session.decisions.clone(),
57 files: session.files_touched.clone(),
58 next_steps: session.next_steps.clone(),
59 test_results: session.test_results.clone(),
60 },
61 summaries,
62 knowledge,
63 }
64}
65
66fn load_knowledge_facts(project_root: &str) -> Vec<KnowledgeFact> {
67 let Some(pk) = crate::core::knowledge::ProjectKnowledge::load(project_root) else {
68 return Vec::new();
69 };
70 pk.facts
71 .iter()
72 .filter(|f| f.is_current() && f.confidence >= 0.5)
73 .take(100)
74 .map(|f| KnowledgeFact {
75 category: f.category.clone(),
76 key: f.key.clone(),
77 value: f.value.clone(),
78 confidence: f.confidence,
79 created_at: f.created_at,
80 })
81 .collect()
82}
83
84fn default_path(project_root: &str, session_id: &str) -> PathBuf {
85 let hash = crate::core::project_hash::hash_project_root(project_root);
86 let dir = crate::core::data_dir::lean_ctx_data_dir()
87 .unwrap_or_else(|_| PathBuf::from(".lean-ctx"))
88 .join("packages")
89 .join(hash);
90 let short = session_id.split('-').next().unwrap_or(session_id);
91 dir.join(format!("{short}.ctx.json"))
92}