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