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(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}