Skip to main content

lean_ctx/tools/
ctx_share.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Serialize, Deserialize, Clone)]
5struct SharedContext {
6    from_agent: String,
7    to_agent: Option<String>,
8    files: Vec<SharedFile>,
9    message: Option<String>,
10    timestamp: String,
11}
12
13#[derive(Serialize, Deserialize, Clone)]
14struct SharedFile {
15    path: String,
16    content: String,
17    mode: String,
18    tokens: usize,
19}
20
21fn shared_dir(project_root: &str) -> PathBuf {
22    let hash = crate::core::project_hash::hash_project_root(project_root);
23    crate::core::data_dir::lean_ctx_data_dir()
24        .unwrap_or_else(|_| PathBuf::from("."))
25        .join("agents")
26        .join("shared")
27        .join(hash)
28}
29
30pub fn handle(
31    action: &str,
32    from_agent: Option<&str>,
33    to_agent: Option<&str>,
34    paths: Option<&str>,
35    message: Option<&str>,
36    cache: &crate::core::cache::SessionCache,
37    project_root: &str,
38) -> String {
39    match action {
40        "push" => handle_push(from_agent, to_agent, paths, message, cache, project_root),
41        "pull" => handle_pull(from_agent, project_root),
42        "list" => handle_list(project_root),
43        "clear" => handle_clear(from_agent, project_root),
44        _ => format!("Unknown action: {action}. Use: push, pull, list, clear"),
45    }
46}
47
48fn handle_push(
49    from_agent: Option<&str>,
50    to_agent: Option<&str>,
51    paths: Option<&str>,
52    message: Option<&str>,
53    cache: &crate::core::cache::SessionCache,
54    project_root: &str,
55) -> String {
56    let Some(from) = from_agent else {
57        return "Error: from_agent is required (register first via ctx_agent)".to_string();
58    };
59
60    let path_list: Vec<&str> = match paths {
61        Some(p) => p.split(',').map(str::trim).collect(),
62        None => return "Error: paths is required (comma-separated file paths)".to_string(),
63    };
64
65    let mut shared_files = Vec::new();
66    let mut not_found = Vec::new();
67
68    for path in &path_list {
69        if let Some(entry) = cache.get(path) {
70            if let Some(content) = entry.content() {
71                shared_files.push(SharedFile {
72                    path: entry.path.clone(),
73                    content,
74                    mode: "full".to_string(),
75                    tokens: entry.original_tokens,
76                });
77            }
78        } else {
79            not_found.push(*path);
80        }
81    }
82
83    if shared_files.is_empty() {
84        return format!(
85            "No cached files found to share. Files must be read first via ctx_read.\nNot found: {}",
86            not_found.join(", ")
87        );
88    }
89
90    let context = SharedContext {
91        from_agent: from.to_string(),
92        to_agent: to_agent.map(String::from),
93        files: shared_files.clone(),
94        message: message.map(String::from),
95        timestamp: chrono::Utc::now().to_rfc3339(),
96    };
97
98    let dir = shared_dir(project_root);
99    let _ = std::fs::create_dir_all(&dir);
100
101    let filename = format!(
102        "{}_{}.json",
103        from,
104        chrono::Utc::now().format("%Y%m%d_%H%M%S")
105    );
106    let path = dir.join(&filename);
107
108    match serde_json::to_string_pretty(&context) {
109        Ok(json) => {
110            if let Err(e) = std::fs::write(&path, json) {
111                return format!("Error writing shared context: {e}");
112            }
113        }
114        Err(e) => return format!("Error serializing shared context: {e}"),
115    }
116
117    let total_tokens: usize = shared_files.iter().map(|f| f.tokens).sum();
118    let mut result = format!(
119        "Shared {} files ({} tokens) from {from}",
120        shared_files.len(),
121        total_tokens
122    );
123
124    if let Some(target) = to_agent {
125        result.push_str(&format!(" → {target}"));
126    } else {
127        result.push_str(" → all agents (broadcast)");
128    }
129
130    if !not_found.is_empty() {
131        result.push_str(&format!(
132            "\nNot in cache (skipped): {}",
133            not_found.join(", ")
134        ));
135    }
136
137    result
138}
139
140fn handle_pull(agent_id: Option<&str>, project_root: &str) -> String {
141    let dir = shared_dir(project_root);
142    if !dir.exists() {
143        return "No shared contexts available.".to_string();
144    }
145
146    let my_id = agent_id.unwrap_or("anonymous");
147    let mut entries: Vec<SharedContext> = Vec::new();
148
149    if let Ok(readdir) = std::fs::read_dir(&dir) {
150        for entry in readdir.flatten() {
151            if let Ok(content) = std::fs::read_to_string(entry.path()) {
152                if let Ok(ctx) = serde_json::from_str::<SharedContext>(&content) {
153                    let is_for_me =
154                        ctx.to_agent.is_none() || ctx.to_agent.as_deref() == Some(my_id);
155                    let is_not_from_me = ctx.from_agent != my_id;
156
157                    if is_for_me && is_not_from_me {
158                        entries.push(ctx);
159                    }
160                }
161            }
162        }
163    }
164
165    if entries.is_empty() {
166        return "No shared contexts for you.".to_string();
167    }
168
169    entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
170
171    let mut out = format!("Shared contexts available ({}):\n", entries.len());
172    for ctx in &entries {
173        let file_list: Vec<&str> = ctx.files.iter().map(|f| f.path.as_str()).collect();
174        let total_tokens: usize = ctx.files.iter().map(|f| f.tokens).sum();
175        out.push_str(&format!(
176            "\n  From: {} ({})\n  Files: {} ({} tokens)\n  {}\n",
177            ctx.from_agent,
178            &ctx.timestamp[..19],
179            file_list.join(", "),
180            total_tokens,
181            ctx.message
182                .as_deref()
183                .map(|m| format!("Message: {m}"))
184                .unwrap_or_default(),
185        ));
186    }
187
188    let total_files: usize = entries.iter().map(|e| e.files.len()).sum();
189    out.push_str(&format!(
190        "\nTotal: {} contexts, {} files. Use ctx_read on pulled files to load them into your cache.",
191        entries.len(),
192        total_files
193    ));
194
195    out
196}
197
198fn handle_list(project_root: &str) -> String {
199    let dir = shared_dir(project_root);
200    if !dir.exists() {
201        return "No shared contexts.".to_string();
202    }
203
204    let mut count = 0;
205    let mut total_files = 0;
206    let mut out = String::from("Shared context store:\n");
207
208    if let Ok(readdir) = std::fs::read_dir(&dir) {
209        for entry in readdir.flatten() {
210            if let Ok(content) = std::fs::read_to_string(entry.path()) {
211                if let Ok(ctx) = serde_json::from_str::<SharedContext>(&content) {
212                    count += 1;
213                    total_files += ctx.files.len();
214                    let target = ctx.to_agent.as_deref().unwrap_or("broadcast");
215                    out.push_str(&format!(
216                        "  {} → {} ({} files, {})\n",
217                        ctx.from_agent,
218                        target,
219                        ctx.files.len(),
220                        &ctx.timestamp[..19]
221                    ));
222                }
223            }
224        }
225    }
226
227    if count == 0 {
228        return "No shared contexts.".to_string();
229    }
230
231    out.push_str(&format!("\nTotal: {count} shares, {total_files} files"));
232    out
233}
234
235fn handle_clear(agent_id: Option<&str>, project_root: &str) -> String {
236    let dir = shared_dir(project_root);
237    if !dir.exists() {
238        return "Nothing to clear.".to_string();
239    }
240
241    let my_id = agent_id.unwrap_or("anonymous");
242    let mut removed = 0;
243
244    if let Ok(readdir) = std::fs::read_dir(&dir) {
245        for entry in readdir.flatten() {
246            if let Ok(content) = std::fs::read_to_string(entry.path()) {
247                if let Ok(ctx) = serde_json::from_str::<SharedContext>(&content) {
248                    if ctx.from_agent == my_id {
249                        let _ = std::fs::remove_file(entry.path());
250                        removed += 1;
251                    }
252                }
253            }
254        }
255    }
256
257    format!("Cleared {removed} shared context(s) from {my_id}")
258}