lean_ctx/tools/
ctx_share.rs1use 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}