Skip to main content

lean_ctx/core/context_package/
export.rs

1//! Export a context package from the current session (#293).
2
3use std::path::{Path, PathBuf};
4
5use chrono::Utc;
6
7use super::bundle::{ContextPackage, FORMAT_VERSION, KnowledgeFact, PackageMetadata, SessionSlice};
8use crate::core::session::SessionState;
9use crate::core::session_summary;
10
11/// Export the current session as a context package (JSON file).
12///
13/// Returns the path where the package was written.
14pub 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}