1use md5::{Digest, Md5};
2use rmcp::handler::server::ServerHandler;
3use rmcp::model::*;
4use rmcp::service::{RequestContext, RoleServer};
5use rmcp::ErrorData;
6use serde_json::Value;
7
8use crate::tools::{CrpMode, LeanCtxServer};
9
10impl ServerHandler for LeanCtxServer {
11 fn get_info(&self) -> ServerInfo {
12 let capabilities = ServerCapabilities::builder().enable_tools().build();
13
14 let instructions = crate::instructions::build_instructions(self.crp_mode);
15
16 InitializeResult::new(capabilities)
17 .with_server_info(Implementation::new("lean-ctx", env!("CARGO_PKG_VERSION")))
18 .with_instructions(instructions)
19 }
20
21 async fn initialize(
22 &self,
23 request: InitializeRequestParams,
24 _context: RequestContext<RoleServer>,
25 ) -> Result<InitializeResult, ErrorData> {
26 let name = request.client_info.name.clone();
27 tracing::info!("MCP client connected: {:?}", name);
28 *self.client_name.write().await = name.clone();
29
30 let derived_root = derive_project_root_from_cwd();
31 let cwd_str = std::env::current_dir()
32 .ok()
33 .map(|p| p.to_string_lossy().to_string())
34 .unwrap_or_default();
35 {
36 let mut session = self.session.write().await;
37 if !cwd_str.is_empty() {
38 session.shell_cwd = Some(cwd_str.clone());
39 }
40 if let Some(ref root) = derived_root {
41 session.project_root = Some(root.clone());
42 tracing::info!("Project root set to: {root}");
43 } else if let Some(ref root) = session.project_root {
44 let root_path = std::path::Path::new(root);
45 let root_has_marker = has_project_marker(root_path);
46 let root_str = root_path.to_string_lossy();
47 let root_suspicious = root_str.contains("/.claude")
48 || root_str.contains("/.codex")
49 || root_str.contains("/var/folders/")
50 || root_str.contains("/tmp/")
51 || root_str.contains("\\.claude")
52 || root_str.contains("\\.codex")
53 || root_str.contains("\\AppData\\Local\\Temp")
54 || root_str.contains("\\Temp\\");
55 if root_suspicious && !root_has_marker {
56 session.project_root = None;
57 }
58 }
59 let _ = session.save();
60 }
61
62 tokio::task::spawn_blocking(|| {
63 if let Some(home) = dirs::home_dir() {
64 let _ = crate::rules_inject::inject_all_rules(&home);
65 }
66 crate::hooks::refresh_installed_hooks();
67 crate::core::version_check::check_background();
68 });
69
70 let instructions =
71 crate::instructions::build_instructions_with_client(self.crp_mode, &name);
72 let capabilities = ServerCapabilities::builder().enable_tools().build();
73
74 Ok(InitializeResult::new(capabilities)
75 .with_server_info(Implementation::new("lean-ctx", env!("CARGO_PKG_VERSION")))
76 .with_instructions(instructions))
77 }
78
79 async fn list_tools(
80 &self,
81 _request: Option<PaginatedRequestParams>,
82 _context: RequestContext<RoleServer>,
83 ) -> Result<ListToolsResult, ErrorData> {
84 let all_tools = if crate::tool_defs::is_lazy_mode() {
85 crate::tool_defs::lazy_tool_defs()
86 } else if std::env::var("LEAN_CTX_UNIFIED").is_ok()
87 && std::env::var("LEAN_CTX_FULL_TOOLS").is_err()
88 {
89 crate::tool_defs::unified_tool_defs()
90 } else {
91 crate::tool_defs::granular_tool_defs()
92 };
93
94 let disabled = crate::core::config::Config::load().disabled_tools_effective();
95 let tools = if disabled.is_empty() {
96 all_tools
97 } else {
98 all_tools
99 .into_iter()
100 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
101 .collect()
102 };
103
104 let tools = {
105 let active = self.workflow.read().await.clone();
106 if let Some(run) = active {
107 if let Some(state) = run.spec.state(&run.current) {
108 if let Some(allowed) = &state.allowed_tools {
109 let mut allow: std::collections::HashSet<&str> =
110 allowed.iter().map(|s| s.as_str()).collect();
111 allow.insert("ctx");
112 allow.insert("ctx_workflow");
113 return Ok(ListToolsResult {
114 tools: tools
115 .into_iter()
116 .filter(|t| allow.contains(t.name.as_ref()))
117 .collect(),
118 ..Default::default()
119 });
120 }
121 }
122 }
123 tools
124 };
125
126 Ok(ListToolsResult {
127 tools,
128 ..Default::default()
129 })
130 }
131
132 async fn call_tool(
133 &self,
134 request: CallToolRequestParams,
135 _context: RequestContext<RoleServer>,
136 ) -> Result<CallToolResult, ErrorData> {
137 self.check_idle_expiry().await;
138
139 let original_name = request.name.as_ref().to_string();
140 let (resolved_name, resolved_args) = if original_name == "ctx" {
141 let sub = request
142 .arguments
143 .as_ref()
144 .and_then(|a| a.get("tool"))
145 .and_then(|v| v.as_str())
146 .map(|s| s.to_string())
147 .ok_or_else(|| {
148 ErrorData::invalid_params("'tool' is required for ctx meta-tool", None)
149 })?;
150 let tool_name = if sub.starts_with("ctx_") {
151 sub
152 } else {
153 format!("ctx_{sub}")
154 };
155 let mut args = request.arguments.unwrap_or_default();
156 args.remove("tool");
157 (tool_name, Some(args))
158 } else {
159 (original_name, request.arguments)
160 };
161 let name = resolved_name.as_str();
162 let args = &resolved_args;
163
164 if name != "ctx_workflow" {
165 let active = self.workflow.read().await.clone();
166 if let Some(run) = active {
167 if let Some(state) = run.spec.state(&run.current) {
168 if let Some(allowed) = &state.allowed_tools {
169 let allowed_ok = allowed.iter().any(|t| t == name) || name == "ctx";
170 if !allowed_ok {
171 let mut shown = allowed.clone();
172 shown.sort();
173 shown.truncate(30);
174 return Ok(CallToolResult::success(vec![Content::text(format!(
175 "Tool '{name}' blocked by workflow '{}' (state: {}). Allowed ({} shown): {}",
176 run.spec.name,
177 run.current,
178 shown.len(),
179 shown.join(", ")
180 ))]));
181 }
182 }
183 }
184 }
185 }
186
187 let auto_context = {
188 let task = {
189 let session = self.session.read().await;
190 session.task.as_ref().map(|t| t.description.clone())
191 };
192 let project_root = {
193 let session = self.session.read().await;
194 session.project_root.clone()
195 };
196 let mut cache = self.cache.write().await;
197 crate::tools::autonomy::session_lifecycle_pre_hook(
198 &self.autonomy,
199 name,
200 &mut cache,
201 task.as_deref(),
202 project_root.as_deref(),
203 self.crp_mode,
204 )
205 };
206
207 let throttle_result = {
208 let fp = args
209 .as_ref()
210 .map(|a| {
211 crate::core::loop_detection::LoopDetector::fingerprint(
212 &serde_json::Value::Object(a.clone()),
213 )
214 })
215 .unwrap_or_default();
216 let mut detector = self.loop_detector.write().await;
217
218 let is_search = crate::core::loop_detection::LoopDetector::is_search_tool(name);
219 let is_search_shell = name == "ctx_shell" && {
220 let cmd = args
221 .as_ref()
222 .and_then(|a| a.get("command"))
223 .and_then(|v| v.as_str())
224 .unwrap_or("");
225 crate::core::loop_detection::LoopDetector::is_search_shell_command(cmd)
226 };
227
228 if is_search || is_search_shell {
229 let search_pattern = args.as_ref().and_then(|a| {
230 a.get("pattern")
231 .or_else(|| a.get("query"))
232 .and_then(|v| v.as_str())
233 });
234 let shell_pattern = if is_search_shell {
235 args.as_ref()
236 .and_then(|a| a.get("command"))
237 .and_then(|v| v.as_str())
238 .and_then(extract_search_pattern_from_command)
239 } else {
240 None
241 };
242 let pat = search_pattern.or(shell_pattern.as_deref());
243 detector.record_search(name, &fp, pat)
244 } else {
245 detector.record_call(name, &fp)
246 }
247 };
248
249 if throttle_result.level == crate::core::loop_detection::ThrottleLevel::Blocked {
250 let msg = throttle_result.message.unwrap_or_default();
251 return Ok(CallToolResult::success(vec![Content::text(msg)]));
252 }
253
254 let throttle_warning =
255 if throttle_result.level == crate::core::loop_detection::ThrottleLevel::Reduced {
256 throttle_result.message.clone()
257 } else {
258 None
259 };
260
261 let tool_start = std::time::Instant::now();
262 let result_text = match name {
263 "ctx_read" => {
264 let path = match get_str(args, "path") {
265 Some(p) => self
266 .resolve_path(&p)
267 .await
268 .map_err(|e| ErrorData::invalid_params(e, None))?,
269 None => return Err(ErrorData::invalid_params("path is required", None)),
270 };
271 let current_task = {
272 let session = self.session.read().await;
273 session.task.as_ref().map(|t| t.description.clone())
274 };
275 let task_ref = current_task.as_deref();
276 let mut mode = match get_str(args, "mode") {
277 Some(m) => m,
278 None => {
279 let cache = self.cache.read().await;
280 crate::tools::ctx_smart_read::select_mode_with_task(&cache, &path, task_ref)
281 }
282 };
283 let fresh = get_bool(args, "fresh").unwrap_or(false);
284 let start_line = get_int(args, "start_line");
285 if let Some(sl) = start_line {
286 let sl = sl.max(1_i64);
287 mode = format!("lines:{sl}-999999");
288 }
289 let stale = self.is_prompt_cache_stale().await;
290 let effective_mode = LeanCtxServer::upgrade_mode_if_stale(&mode, stale).to_string();
291 let mut cache = self.cache.write().await;
292 let (output, resolved_mode) = if fresh {
293 crate::tools::ctx_read::handle_fresh_with_task_resolved(
294 &mut cache,
295 &path,
296 &effective_mode,
297 self.crp_mode,
298 task_ref,
299 )
300 } else {
301 crate::tools::ctx_read::handle_with_task_resolved(
302 &mut cache,
303 &path,
304 &effective_mode,
305 self.crp_mode,
306 task_ref,
307 )
308 };
309 let stale_note = if effective_mode != mode {
310 format!("[cache stale, {mode}→{effective_mode}]\n")
311 } else {
312 String::new()
313 };
314 let original = cache.get(&path).map_or(0, |e| e.original_tokens);
315 let output_tokens = crate::core::tokens::count_tokens(&output);
316 let saved = original.saturating_sub(output_tokens);
317 let is_cache_hit = output.contains(" cached ");
318 let output = format!("{stale_note}{output}");
319 let file_ref = cache.file_ref_map().get(&path).cloned();
320 drop(cache);
321 let mut ensured_root: Option<String> = None;
322 {
323 let mut session = self.session.write().await;
324 session.touch_file(&path, file_ref.as_deref(), &resolved_mode, original);
325 if is_cache_hit {
326 session.record_cache_hit();
327 }
328 let root_missing = session
329 .project_root
330 .as_deref()
331 .map(|r| r.trim().is_empty())
332 .unwrap_or(true);
333 if root_missing {
334 if let Some(root) = crate::core::protocol::detect_project_root(&path) {
335 session.project_root = Some(root.clone());
336 ensured_root = Some(root.clone());
337 let mut current = self.agent_id.write().await;
338 if current.is_none() {
339 let mut registry =
340 crate::core::agents::AgentRegistry::load_or_create();
341 registry.cleanup_stale(24);
342 let role = std::env::var("LEAN_CTX_AGENT_ROLE").ok();
343 let id = registry.register("mcp", role.as_deref(), &root);
344 let _ = registry.save();
345 *current = Some(id);
346 }
347 }
348 }
349 }
350 if let Some(root) = ensured_root.as_deref() {
351 crate::core::index_orchestrator::ensure_all_background(root);
352 }
353 self.record_call("ctx_read", original, saved, Some(resolved_mode.clone()))
354 .await;
355 crate::core::heatmap::record_file_access(&path, original, saved);
356 {
357 let sig =
358 crate::core::mode_predictor::FileSignature::from_path(&path, original);
359 let density = if output_tokens > 0 {
360 original as f64 / output_tokens as f64
361 } else {
362 1.0
363 };
364 let outcome = crate::core::mode_predictor::ModeOutcome {
365 mode: resolved_mode.clone(),
366 tokens_in: original,
367 tokens_out: output_tokens,
368 density: density.min(1.0),
369 };
370 let mut predictor = crate::core::mode_predictor::ModePredictor::new();
371 predictor.record(sig, outcome);
372 predictor.save();
373
374 let ext = std::path::Path::new(&path)
375 .extension()
376 .and_then(|e| e.to_str())
377 .unwrap_or("")
378 .to_string();
379 let thresholds = crate::core::adaptive_thresholds::thresholds_for_path(&path);
380 let cache = self.cache.read().await;
381 let stats = cache.get_stats();
382 let feedback_outcome = crate::core::feedback::CompressionOutcome {
383 session_id: format!("{}", std::process::id()),
384 language: ext,
385 entropy_threshold: thresholds.bpe_entropy,
386 jaccard_threshold: thresholds.jaccard,
387 total_turns: stats.total_reads as u32,
388 tokens_saved: saved as u64,
389 tokens_original: original as u64,
390 cache_hits: stats.cache_hits as u32,
391 total_reads: stats.total_reads as u32,
392 task_completed: true,
393 timestamp: chrono::Local::now().to_rfc3339(),
394 };
395 drop(cache);
396 let mut store = crate::core::feedback::FeedbackStore::load();
397 store.record_outcome(feedback_outcome);
398 }
399 output
400 }
401 "ctx_multi_read" => {
402 let raw_paths = get_str_array(args, "paths")
403 .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
404 let mut paths = Vec::with_capacity(raw_paths.len());
405 for p in raw_paths {
406 paths.push(
407 self.resolve_path(&p)
408 .await
409 .map_err(|e| ErrorData::invalid_params(e, None))?,
410 );
411 }
412 let mode = get_str(args, "mode").unwrap_or_else(|| "full".to_string());
413 let current_task = {
414 let session = self.session.read().await;
415 session.task.as_ref().map(|t| t.description.clone())
416 };
417 let mut cache = self.cache.write().await;
418 let output = crate::tools::ctx_multi_read::handle_with_task(
419 &mut cache,
420 &paths,
421 &mode,
422 self.crp_mode,
423 current_task.as_deref(),
424 );
425 let mut total_original: usize = 0;
426 for path in &paths {
427 total_original = total_original
428 .saturating_add(cache.get(path).map(|e| e.original_tokens).unwrap_or(0));
429 }
430 let tokens = crate::core::tokens::count_tokens(&output);
431 drop(cache);
432 self.record_call(
433 "ctx_multi_read",
434 total_original,
435 total_original.saturating_sub(tokens),
436 Some(mode),
437 )
438 .await;
439 output
440 }
441 "ctx_tree" => {
442 let path = self
443 .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
444 .await
445 .map_err(|e| ErrorData::invalid_params(e, None))?;
446 let depth = get_int(args, "depth").unwrap_or(3) as usize;
447 let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
448 let (result, original) = crate::tools::ctx_tree::handle(&path, depth, show_hidden);
449 let sent = crate::core::tokens::count_tokens(&result);
450 let saved = original.saturating_sub(sent);
451 self.record_call("ctx_tree", original, saved, None).await;
452 let savings_note = if saved > 0 {
453 format!("\n[saved {saved} tokens vs native ls]")
454 } else {
455 String::new()
456 };
457 format!("{result}{savings_note}")
458 }
459 "ctx_shell" => {
460 let command = get_str(args, "command")
461 .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
462
463 if let Some(rejection) = crate::tools::ctx_shell::validate_command(&command) {
464 self.record_call("ctx_shell", 0, 0, None).await;
465 return Ok(CallToolResult::success(vec![Content::text(rejection)]));
466 }
467
468 let explicit_cwd = get_str(args, "cwd");
469 let effective_cwd = {
470 let session = self.session.read().await;
471 session.effective_cwd(explicit_cwd.as_deref())
472 };
473
474 let ensured_root = {
475 let mut session = self.session.write().await;
476 session.update_shell_cwd(&command);
477 let root_missing = session
478 .project_root
479 .as_deref()
480 .map(|r| r.trim().is_empty())
481 .unwrap_or(true);
482 if !root_missing {
483 None
484 } else {
485 let home = dirs::home_dir().map(|h| h.to_string_lossy().to_string());
486 crate::core::protocol::detect_project_root(&effective_cwd).and_then(|r| {
487 if home.as_deref() == Some(r.as_str()) {
488 None
489 } else {
490 session.project_root = Some(r.clone());
491 Some(r)
492 }
493 })
494 }
495 };
496 if let Some(root) = ensured_root.as_deref() {
497 crate::core::index_orchestrator::ensure_all_background(root);
498 let mut current = self.agent_id.write().await;
499 if current.is_none() {
500 let mut registry = crate::core::agents::AgentRegistry::load_or_create();
501 registry.cleanup_stale(24);
502 let role = std::env::var("LEAN_CTX_AGENT_ROLE").ok();
503 let id = registry.register("mcp", role.as_deref(), root);
504 let _ = registry.save();
505 *current = Some(id);
506 }
507 }
508
509 let raw = get_bool(args, "raw").unwrap_or(false)
510 || std::env::var("LEAN_CTX_DISABLED").is_ok();
511 let cmd_clone = command.clone();
512 let cwd_clone = effective_cwd.clone();
513 let crp_mode = self.crp_mode;
514
515 let (result_out, original, saved, tee_hint) =
516 tokio::task::spawn_blocking(move || {
517 let (output, _real_exit_code) = execute_command_in(&cmd_clone, &cwd_clone);
518
519 if raw {
521 let tokens = crate::core::tokens::count_tokens(&output);
522 (output, tokens, 0, String::new())
523 } else {
524 let result =
525 crate::tools::ctx_shell::handle(&cmd_clone, &output, crp_mode);
526 let original = crate::core::tokens::count_tokens(&output);
527 let sent = crate::core::tokens::count_tokens(&result);
528 let saved = original.saturating_sub(sent);
529
530 let cfg = crate::core::config::Config::load();
531 let tee_hint = match cfg.tee_mode {
532 crate::core::config::TeeMode::Always => {
533 crate::shell::save_tee(&cmd_clone, &output)
534 .map(|p| format!("\n[full output: {p}]"))
535 .unwrap_or_default()
536 }
537 crate::core::config::TeeMode::Failures
538 if !output.trim().is_empty()
539 && (output.contains("error")
540 || output.contains("Error")
541 || output.contains("ERROR")) =>
542 {
543 crate::shell::save_tee(&cmd_clone, &output)
544 .map(|p| format!("\n[full output: {p}]"))
545 .unwrap_or_default()
546 }
547 _ => String::new(),
548 };
549
550 (result, original, saved, tee_hint)
556 }
557 })
558 .await
559 .unwrap_or_else(|e| {
560 (
561 format!("ERROR: shell task failed: {e}"),
562 0,
563 0,
564 String::new(),
565 )
566 });
567
568 self.record_call("ctx_shell", original, saved, None).await;
569
570 let savings_note = if !raw && saved > 0 {
571 format!("\n[saved {saved} tokens vs native Shell]")
572 } else {
573 String::new()
574 };
575
576 format!("{result_out}{savings_note}{tee_hint}")
577 }
578 "ctx_search" => {
579 let pattern = get_str(args, "pattern")
580 .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
581 let path = self
582 .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
583 .await
584 .map_err(|e| ErrorData::invalid_params(e, None))?;
585 let ext = get_str(args, "ext");
586 let max = get_int(args, "max_results").unwrap_or(20) as usize;
587 let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
588 let crp = self.crp_mode;
589 let respect = !no_gitignore;
590 let search_result = tokio::time::timeout(
591 std::time::Duration::from_secs(30),
592 tokio::task::spawn_blocking(move || {
593 crate::tools::ctx_search::handle(
594 &pattern,
595 &path,
596 ext.as_deref(),
597 max,
598 crp,
599 respect,
600 )
601 }),
602 )
603 .await;
604 let (result, original) = match search_result {
605 Ok(Ok(r)) => r,
606 Ok(Err(e)) => {
607 return Err(ErrorData::internal_error(
608 format!("search task failed: {e}"),
609 None,
610 ))
611 }
612 Err(_) => {
613 let msg = "ctx_search timed out after 30s. Try narrowing the search:\n\
614 • Use a more specific pattern\n\
615 • Specify ext= to limit file types\n\
616 • Specify a subdirectory in path=";
617 self.record_call("ctx_search", 0, 0, None).await;
618 return Ok(CallToolResult::success(vec![Content::text(msg)]));
619 }
620 };
621 let sent = crate::core::tokens::count_tokens(&result);
622 let saved = original.saturating_sub(sent);
623 self.record_call("ctx_search", original, saved, None).await;
624 let savings_note = if saved > 0 {
625 format!("\n[saved {saved} tokens vs native Grep]")
626 } else {
627 String::new()
628 };
629 format!("{result}{savings_note}")
630 }
631 "ctx_compress" => {
632 let include_sigs = get_bool(args, "include_signatures").unwrap_or(true);
633 let cache = self.cache.read().await;
634 let result =
635 crate::tools::ctx_compress::handle(&cache, include_sigs, self.crp_mode);
636 drop(cache);
637 self.record_call("ctx_compress", 0, 0, None).await;
638 result
639 }
640 "ctx_benchmark" => {
641 let path = match get_str(args, "path") {
642 Some(p) => self
643 .resolve_path(&p)
644 .await
645 .map_err(|e| ErrorData::invalid_params(e, None))?,
646 None => return Err(ErrorData::invalid_params("path is required", None)),
647 };
648 let action = get_str(args, "action").unwrap_or_default();
649 let result = if action == "project" {
650 let fmt = get_str(args, "format").unwrap_or_default();
651 let bench = crate::core::benchmark::run_project_benchmark(&path);
652 match fmt.as_str() {
653 "json" => crate::core::benchmark::format_json(&bench),
654 "markdown" | "md" => crate::core::benchmark::format_markdown(&bench),
655 _ => crate::core::benchmark::format_terminal(&bench),
656 }
657 } else {
658 crate::tools::ctx_benchmark::handle(&path, self.crp_mode)
659 };
660 self.record_call("ctx_benchmark", 0, 0, None).await;
661 result
662 }
663 "ctx_metrics" => {
664 let cache = self.cache.read().await;
665 let calls = self.tool_calls.read().await;
666 let result = crate::tools::ctx_metrics::handle(&cache, &calls, self.crp_mode);
667 drop(cache);
668 drop(calls);
669 self.record_call("ctx_metrics", 0, 0, None).await;
670 result
671 }
672 "ctx_analyze" => {
673 let path = match get_str(args, "path") {
674 Some(p) => self
675 .resolve_path(&p)
676 .await
677 .map_err(|e| ErrorData::invalid_params(e, None))?,
678 None => return Err(ErrorData::invalid_params("path is required", None)),
679 };
680 let result = crate::tools::ctx_analyze::handle(&path, self.crp_mode);
681 self.record_call("ctx_analyze", 0, 0, None).await;
682 result
683 }
684 "ctx_discover" => {
685 let limit = get_int(args, "limit").unwrap_or(15) as usize;
686 let history = crate::cli::load_shell_history_pub();
687 let result = crate::tools::ctx_discover::discover_from_history(&history, limit);
688 self.record_call("ctx_discover", 0, 0, None).await;
689 result
690 }
691 "ctx_smart_read" => {
692 let path = match get_str(args, "path") {
693 Some(p) => self
694 .resolve_path(&p)
695 .await
696 .map_err(|e| ErrorData::invalid_params(e, None))?,
697 None => return Err(ErrorData::invalid_params("path is required", None)),
698 };
699 let mut cache = self.cache.write().await;
700 let output = crate::tools::ctx_smart_read::handle(&mut cache, &path, self.crp_mode);
701 let original = cache.get(&path).map_or(0, |e| e.original_tokens);
702 let tokens = crate::core::tokens::count_tokens(&output);
703 drop(cache);
704 self.record_call(
705 "ctx_smart_read",
706 original,
707 original.saturating_sub(tokens),
708 Some("auto".to_string()),
709 )
710 .await;
711 output
712 }
713 "ctx_delta" => {
714 let path = match get_str(args, "path") {
715 Some(p) => self
716 .resolve_path(&p)
717 .await
718 .map_err(|e| ErrorData::invalid_params(e, None))?,
719 None => return Err(ErrorData::invalid_params("path is required", None)),
720 };
721 let mut cache = self.cache.write().await;
722 let output = crate::tools::ctx_delta::handle(&mut cache, &path);
723 let original = cache.get(&path).map_or(0, |e| e.original_tokens);
724 let tokens = crate::core::tokens::count_tokens(&output);
725 drop(cache);
726 {
727 let mut session = self.session.write().await;
728 session.mark_modified(&path);
729 }
730 self.record_call(
731 "ctx_delta",
732 original,
733 original.saturating_sub(tokens),
734 Some("delta".to_string()),
735 )
736 .await;
737 output
738 }
739 "ctx_edit" => {
740 let path = match get_str(args, "path") {
741 Some(p) => self
742 .resolve_path(&p)
743 .await
744 .map_err(|e| ErrorData::invalid_params(e, None))?,
745 None => return Err(ErrorData::invalid_params("path is required", None)),
746 };
747 let old_string = get_str(args, "old_string").unwrap_or_default();
748 let new_string = get_str(args, "new_string")
749 .ok_or_else(|| ErrorData::invalid_params("new_string is required", None))?;
750 let replace_all = args
751 .as_ref()
752 .and_then(|a| a.get("replace_all"))
753 .and_then(|v| v.as_bool())
754 .unwrap_or(false);
755 let create = args
756 .as_ref()
757 .and_then(|a| a.get("create"))
758 .and_then(|v| v.as_bool())
759 .unwrap_or(false);
760
761 let mut cache = self.cache.write().await;
762 let output = crate::tools::ctx_edit::handle(
763 &mut cache,
764 crate::tools::ctx_edit::EditParams {
765 path: path.clone(),
766 old_string,
767 new_string,
768 replace_all,
769 create,
770 },
771 );
772 drop(cache);
773
774 {
775 let mut session = self.session.write().await;
776 session.mark_modified(&path);
777 }
778 self.record_call("ctx_edit", 0, 0, None).await;
779 output
780 }
781 "ctx_dedup" => {
782 let action = get_str(args, "action").unwrap_or_default();
783 if action == "apply" {
784 let mut cache = self.cache.write().await;
785 let result = crate::tools::ctx_dedup::handle_action(&mut cache, &action);
786 drop(cache);
787 self.record_call("ctx_dedup", 0, 0, None).await;
788 result
789 } else {
790 let cache = self.cache.read().await;
791 let result = crate::tools::ctx_dedup::handle(&cache);
792 drop(cache);
793 self.record_call("ctx_dedup", 0, 0, None).await;
794 result
795 }
796 }
797 "ctx_fill" => {
798 let raw_paths = get_str_array(args, "paths")
799 .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
800 let mut paths = Vec::with_capacity(raw_paths.len());
801 for p in raw_paths {
802 paths.push(
803 self.resolve_path(&p)
804 .await
805 .map_err(|e| ErrorData::invalid_params(e, None))?,
806 );
807 }
808 let budget = get_int(args, "budget")
809 .ok_or_else(|| ErrorData::invalid_params("budget is required", None))?
810 as usize;
811 let task = get_str(args, "task");
812 let mut cache = self.cache.write().await;
813 let output = crate::tools::ctx_fill::handle(
814 &mut cache,
815 &paths,
816 budget,
817 self.crp_mode,
818 task.as_deref(),
819 );
820 drop(cache);
821 self.record_call("ctx_fill", 0, 0, Some(format!("budget:{budget}")))
822 .await;
823 output
824 }
825 "ctx_intent" => {
826 let query = get_str(args, "query")
827 .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
828 let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
829 let mut cache = self.cache.write().await;
830 let output =
831 crate::tools::ctx_intent::handle(&mut cache, &query, &root, self.crp_mode);
832 drop(cache);
833 {
834 let mut session = self.session.write().await;
835 session.set_task(&query, Some("intent"));
836 }
837 self.record_call("ctx_intent", 0, 0, Some("semantic".to_string()))
838 .await;
839 output
840 }
841 "ctx_response" => {
842 let text = get_str(args, "text")
843 .ok_or_else(|| ErrorData::invalid_params("text is required", None))?;
844 let output = crate::tools::ctx_response::handle(&text, self.crp_mode);
845 self.record_call("ctx_response", 0, 0, None).await;
846 output
847 }
848 "ctx_context" => {
849 let cache = self.cache.read().await;
850 let turn = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
851 let result = crate::tools::ctx_context::handle_status(&cache, turn, self.crp_mode);
852 drop(cache);
853 self.record_call("ctx_context", 0, 0, None).await;
854 result
855 }
856 "ctx_graph" => {
857 let action = get_str(args, "action")
858 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
859 let path = match get_str(args, "path") {
860 Some(p) => Some(
861 self.resolve_path(&p)
862 .await
863 .map_err(|e| ErrorData::invalid_params(e, None))?,
864 ),
865 None => None,
866 };
867 let root = self
868 .resolve_path(&get_str(args, "project_root").unwrap_or_else(|| ".".to_string()))
869 .await
870 .map_err(|e| ErrorData::invalid_params(e, None))?;
871 let crp_mode = self.crp_mode;
872 let action_for_record = action.clone();
873 let mut cache = self.cache.write().await;
874 let result = crate::tools::ctx_graph::handle(
875 &action,
876 path.as_deref(),
877 &root,
878 &mut cache,
879 crp_mode,
880 );
881 drop(cache);
882 self.record_call("ctx_graph", 0, 0, Some(action_for_record))
883 .await;
884 result
885 }
886 "ctx_cache" => {
887 let action = get_str(args, "action")
888 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
889 let mut cache = self.cache.write().await;
890 let result = match action.as_str() {
891 "status" => {
892 let entries = cache.get_all_entries();
893 if entries.is_empty() {
894 "Cache empty — no files tracked.".to_string()
895 } else {
896 let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
897 for (path, entry) in &entries {
898 let fref = cache
899 .file_ref_map()
900 .get(*path)
901 .map(|s| s.as_str())
902 .unwrap_or("F?");
903 lines.push(format!(
904 " {fref}={} [{}L, {}t, read {}x]",
905 crate::core::protocol::shorten_path(path),
906 entry.line_count,
907 entry.original_tokens,
908 entry.read_count
909 ));
910 }
911 lines.join("\n")
912 }
913 }
914 "clear" => {
915 let count = cache.clear();
916 format!("Cache cleared — {count} file(s) removed. Next ctx_read will return full content.")
917 }
918 "invalidate" => {
919 let path = match get_str(args, "path") {
920 Some(p) => self
921 .resolve_path(&p)
922 .await
923 .map_err(|e| ErrorData::invalid_params(e, None))?,
924 None => {
925 return Err(ErrorData::invalid_params(
926 "path is required for invalidate",
927 None,
928 ))
929 }
930 };
931 if cache.invalidate(&path) {
932 format!(
933 "Invalidated cache for {}. Next ctx_read will return full content.",
934 crate::core::protocol::shorten_path(&path)
935 )
936 } else {
937 format!(
938 "{} was not in cache.",
939 crate::core::protocol::shorten_path(&path)
940 )
941 }
942 }
943 _ => "Unknown action. Use: status, clear, invalidate".to_string(),
944 };
945 drop(cache);
946 self.record_call("ctx_cache", 0, 0, Some(action)).await;
947 result
948 }
949 "ctx_session" => {
950 let action = get_str(args, "action")
951 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
952 let value = get_str(args, "value");
953 let sid = get_str(args, "session_id");
954 let mut session = self.session.write().await;
955 let result = crate::tools::ctx_session::handle(
956 &mut session,
957 &action,
958 value.as_deref(),
959 sid.as_deref(),
960 );
961 drop(session);
962 self.record_call("ctx_session", 0, 0, Some(action)).await;
963 result
964 }
965 "ctx_knowledge" => {
966 let action = get_str(args, "action")
967 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
968 let category = get_str(args, "category");
969 let key = get_str(args, "key");
970 let value = get_str(args, "value");
971 let query = get_str(args, "query");
972 let pattern_type = get_str(args, "pattern_type");
973 let examples = get_str_array(args, "examples");
974 let confidence: Option<f32> = args
975 .as_ref()
976 .and_then(|a| a.get("confidence"))
977 .and_then(|v| v.as_f64())
978 .map(|v| v as f32);
979
980 let session = self.session.read().await;
981 let session_id = session.id.clone();
982 let project_root = session.project_root.clone().unwrap_or_else(|| {
983 std::env::current_dir()
984 .map(|p| p.to_string_lossy().to_string())
985 .unwrap_or_else(|_| "unknown".to_string())
986 });
987 drop(session);
988
989 if action == "gotcha" {
990 let trigger = get_str(args, "trigger").unwrap_or_default();
991 let resolution = get_str(args, "resolution").unwrap_or_default();
992 let severity = get_str(args, "severity").unwrap_or_default();
993 let cat = category.as_deref().unwrap_or("convention");
994
995 if trigger.is_empty() || resolution.is_empty() {
996 self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
997 return Ok(CallToolResult::success(vec![Content::text(
998 "ERROR: trigger and resolution are required for gotcha action",
999 )]));
1000 }
1001
1002 let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
1003 let msg = match store.report_gotcha(
1004 &trigger,
1005 &resolution,
1006 cat,
1007 &severity,
1008 &session_id,
1009 ) {
1010 Some(gotcha) => {
1011 let conf = (gotcha.confidence * 100.0) as u32;
1012 let label = gotcha.category.short_label();
1013 format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
1014 }
1015 None => format!(
1016 "Gotcha noted: {trigger} (evicted by higher-confidence entries)"
1017 ),
1018 };
1019 let _ = store.save(&project_root);
1020 self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
1021 return Ok(CallToolResult::success(vec![Content::text(msg)]));
1022 }
1023
1024 let result = crate::tools::ctx_knowledge::handle(
1025 &project_root,
1026 &action,
1027 category.as_deref(),
1028 key.as_deref(),
1029 value.as_deref(),
1030 query.as_deref(),
1031 &session_id,
1032 pattern_type.as_deref(),
1033 examples,
1034 confidence,
1035 );
1036 self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
1037 result
1038 }
1039 "ctx_agent" => {
1040 let action = get_str(args, "action")
1041 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1042 let agent_type = get_str(args, "agent_type");
1043 let role = get_str(args, "role");
1044 let message = get_str(args, "message");
1045 let category = get_str(args, "category");
1046 let to_agent = get_str(args, "to_agent");
1047 let status = get_str(args, "status");
1048
1049 let session = self.session.read().await;
1050 let project_root = session.project_root.clone().unwrap_or_else(|| {
1051 std::env::current_dir()
1052 .map(|p| p.to_string_lossy().to_string())
1053 .unwrap_or_else(|_| "unknown".to_string())
1054 });
1055 drop(session);
1056
1057 let current_agent_id = self.agent_id.read().await.clone();
1058 let result = crate::tools::ctx_agent::handle(
1059 &action,
1060 agent_type.as_deref(),
1061 role.as_deref(),
1062 &project_root,
1063 current_agent_id.as_deref(),
1064 message.as_deref(),
1065 category.as_deref(),
1066 to_agent.as_deref(),
1067 status.as_deref(),
1068 );
1069
1070 if action == "register" {
1071 if let Some(id) = result.split(':').nth(1) {
1072 let id = id.split_whitespace().next().unwrap_or("").to_string();
1073 if !id.is_empty() {
1074 *self.agent_id.write().await = Some(id);
1075 }
1076 }
1077 }
1078
1079 self.record_call("ctx_agent", 0, 0, Some(action)).await;
1080 result
1081 }
1082 "ctx_share" => {
1083 let action = get_str(args, "action")
1084 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1085 let to_agent = get_str(args, "to_agent");
1086 let paths = get_str(args, "paths");
1087 let message = get_str(args, "message");
1088
1089 let from_agent = self.agent_id.read().await.clone();
1090 let cache = self.cache.read().await;
1091 let result = crate::tools::ctx_share::handle(
1092 &action,
1093 from_agent.as_deref(),
1094 to_agent.as_deref(),
1095 paths.as_deref(),
1096 message.as_deref(),
1097 &cache,
1098 );
1099 drop(cache);
1100
1101 self.record_call("ctx_share", 0, 0, Some(action)).await;
1102 result
1103 }
1104 "ctx_overview" => {
1105 let task = get_str(args, "task");
1106 let resolved_path = match get_str(args, "path") {
1107 Some(p) => Some(
1108 self.resolve_path(&p)
1109 .await
1110 .map_err(|e| ErrorData::invalid_params(e, None))?,
1111 ),
1112 None => {
1113 let session = self.session.read().await;
1114 session.project_root.clone()
1115 }
1116 };
1117 let cache = self.cache.read().await;
1118 let crp_mode = self.crp_mode;
1119 let result = crate::tools::ctx_overview::handle(
1120 &cache,
1121 task.as_deref(),
1122 resolved_path.as_deref(),
1123 crp_mode,
1124 );
1125 drop(cache);
1126 self.record_call("ctx_overview", 0, 0, Some("overview".to_string()))
1127 .await;
1128 result
1129 }
1130 "ctx_preload" => {
1131 let task = get_str(args, "task").unwrap_or_default();
1132 let resolved_path = match get_str(args, "path") {
1133 Some(p) => Some(
1134 self.resolve_path(&p)
1135 .await
1136 .map_err(|e| ErrorData::invalid_params(e, None))?,
1137 ),
1138 None => {
1139 let session = self.session.read().await;
1140 session.project_root.clone()
1141 }
1142 };
1143 let mut cache = self.cache.write().await;
1144 let result = crate::tools::ctx_preload::handle(
1145 &mut cache,
1146 &task,
1147 resolved_path.as_deref(),
1148 self.crp_mode,
1149 );
1150 drop(cache);
1151 self.record_call("ctx_preload", 0, 0, Some("preload".to_string()))
1152 .await;
1153 result
1154 }
1155 "ctx_prefetch" => {
1156 let root = match get_str(args, "root") {
1157 Some(r) => self
1158 .resolve_path(&r)
1159 .await
1160 .map_err(|e| ErrorData::invalid_params(e, None))?,
1161 None => {
1162 let session = self.session.read().await;
1163 session
1164 .project_root
1165 .clone()
1166 .unwrap_or_else(|| ".".to_string())
1167 }
1168 };
1169 let task = get_str(args, "task");
1170 let changed_files = get_str_array(args, "changed_files");
1171 let budget_tokens = get_int(args, "budget_tokens")
1172 .map(|n| n.max(0) as usize)
1173 .unwrap_or(3000);
1174 let max_files = get_int(args, "max_files").map(|n| n.max(1) as usize);
1175
1176 let mut resolved_changed: Option<Vec<String>> = None;
1177 if let Some(files) = changed_files {
1178 let mut v = Vec::with_capacity(files.len());
1179 for p in files {
1180 v.push(
1181 self.resolve_path(&p)
1182 .await
1183 .map_err(|e| ErrorData::invalid_params(e, None))?,
1184 );
1185 }
1186 resolved_changed = Some(v);
1187 }
1188
1189 let mut cache = self.cache.write().await;
1190 let result = crate::tools::ctx_prefetch::handle(
1191 &mut cache,
1192 &root,
1193 task.as_deref(),
1194 resolved_changed.as_deref(),
1195 budget_tokens,
1196 max_files,
1197 self.crp_mode,
1198 );
1199 drop(cache);
1200 self.record_call("ctx_prefetch", 0, 0, Some("prefetch".to_string()))
1201 .await;
1202 result
1203 }
1204 "ctx_wrapped" => {
1205 let period = get_str(args, "period").unwrap_or_else(|| "week".to_string());
1206 let result = crate::tools::ctx_wrapped::handle(&period);
1207 self.record_call("ctx_wrapped", 0, 0, Some(period)).await;
1208 result
1209 }
1210 "ctx_semantic_search" => {
1211 let query = get_str(args, "query")
1212 .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
1213 let path = self
1214 .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
1215 .await
1216 .map_err(|e| ErrorData::invalid_params(e, None))?;
1217 let top_k = get_int(args, "top_k").unwrap_or(10) as usize;
1218 let action = get_str(args, "action").unwrap_or_default();
1219 let mode = get_str(args, "mode");
1220 let languages = get_str_array(args, "languages");
1221 let path_glob = get_str(args, "path_glob");
1222 let result = if action == "reindex" {
1223 crate::tools::ctx_semantic_search::handle_reindex(&path)
1224 } else {
1225 crate::tools::ctx_semantic_search::handle(
1226 &query,
1227 &path,
1228 top_k,
1229 self.crp_mode,
1230 languages,
1231 path_glob.as_deref(),
1232 mode.as_deref(),
1233 )
1234 };
1235 self.record_call("ctx_semantic_search", 0, 0, Some("semantic".to_string()))
1236 .await;
1237 result
1238 }
1239 "ctx_execute" => {
1240 let action = get_str(args, "action").unwrap_or_default();
1241
1242 let result = if action == "batch" {
1243 let items_str = get_str(args, "items").ok_or_else(|| {
1244 ErrorData::invalid_params("items is required for batch", None)
1245 })?;
1246 let items: Vec<serde_json::Value> =
1247 serde_json::from_str(&items_str).map_err(|e| {
1248 ErrorData::invalid_params(format!("Invalid items JSON: {e}"), None)
1249 })?;
1250 let batch: Vec<(String, String)> = items
1251 .iter()
1252 .filter_map(|item| {
1253 let lang = item.get("language")?.as_str()?.to_string();
1254 let code = item.get("code")?.as_str()?.to_string();
1255 Some((lang, code))
1256 })
1257 .collect();
1258 crate::tools::ctx_execute::handle_batch(&batch)
1259 } else if action == "file" {
1260 let raw_path = get_str(args, "path").ok_or_else(|| {
1261 ErrorData::invalid_params("path is required for action=file", None)
1262 })?;
1263 let path = self.resolve_path(&raw_path).await.map_err(|e| {
1264 ErrorData::invalid_params(format!("path rejected: {e}"), None)
1265 })?;
1266 let intent = get_str(args, "intent");
1267 crate::tools::ctx_execute::handle_file(&path, intent.as_deref())
1268 } else {
1269 let language = get_str(args, "language")
1270 .ok_or_else(|| ErrorData::invalid_params("language is required", None))?;
1271 let code = get_str(args, "code")
1272 .ok_or_else(|| ErrorData::invalid_params("code is required", None))?;
1273 let intent = get_str(args, "intent");
1274 let timeout = get_int(args, "timeout").map(|t| t as u64);
1275 crate::tools::ctx_execute::handle(&language, &code, intent.as_deref(), timeout)
1276 };
1277
1278 self.record_call("ctx_execute", 0, 0, Some(action)).await;
1279 result
1280 }
1281 "ctx_symbol" => {
1282 let sym_name = get_str(args, "name")
1283 .ok_or_else(|| ErrorData::invalid_params("name is required", None))?;
1284 let file = get_str(args, "file");
1285 let kind = get_str(args, "kind");
1286 let session = self.session.read().await;
1287 let project_root = session
1288 .project_root
1289 .clone()
1290 .unwrap_or_else(|| ".".to_string());
1291 drop(session);
1292 let (result, original) = crate::tools::ctx_symbol::handle(
1293 &sym_name,
1294 file.as_deref(),
1295 kind.as_deref(),
1296 &project_root,
1297 );
1298 let sent = crate::core::tokens::count_tokens(&result);
1299 let saved = original.saturating_sub(sent);
1300 self.record_call("ctx_symbol", original, saved, kind).await;
1301 result
1302 }
1303 "ctx_graph_diagram" => {
1304 let file = get_str(args, "file");
1305 let depth = get_int(args, "depth").map(|d| d as usize);
1306 let kind = get_str(args, "kind");
1307 let session = self.session.read().await;
1308 let project_root = session
1309 .project_root
1310 .clone()
1311 .unwrap_or_else(|| ".".to_string());
1312 drop(session);
1313 let result = crate::tools::ctx_graph_diagram::handle(
1314 file.as_deref(),
1315 depth,
1316 kind.as_deref(),
1317 &project_root,
1318 );
1319 self.record_call("ctx_graph_diagram", 0, 0, kind).await;
1320 result
1321 }
1322 "ctx_routes" => {
1323 let method = get_str(args, "method");
1324 let path_prefix = get_str(args, "path");
1325 let session = self.session.read().await;
1326 let project_root = session
1327 .project_root
1328 .clone()
1329 .unwrap_or_else(|| ".".to_string());
1330 drop(session);
1331 let result = crate::tools::ctx_routes::handle(
1332 method.as_deref(),
1333 path_prefix.as_deref(),
1334 &project_root,
1335 );
1336 self.record_call("ctx_routes", 0, 0, None).await;
1337 result
1338 }
1339 "ctx_compress_memory" => {
1340 let path = self
1341 .resolve_path(
1342 &get_str(args, "path")
1343 .ok_or_else(|| ErrorData::invalid_params("path is required", None))?,
1344 )
1345 .await
1346 .map_err(|e| ErrorData::invalid_params(e, None))?;
1347 let result = crate::tools::ctx_compress_memory::handle(&path);
1348 self.record_call("ctx_compress_memory", 0, 0, None).await;
1349 result
1350 }
1351 "ctx_callers" => {
1352 let symbol = get_str(args, "symbol")
1353 .ok_or_else(|| ErrorData::invalid_params("symbol is required", None))?;
1354 let file = get_str(args, "file");
1355 let session = self.session.read().await;
1356 let project_root = session
1357 .project_root
1358 .clone()
1359 .unwrap_or_else(|| ".".to_string());
1360 drop(session);
1361 let result =
1362 crate::tools::ctx_callers::handle(&symbol, file.as_deref(), &project_root);
1363 self.record_call("ctx_callers", 0, 0, None).await;
1364 result
1365 }
1366 "ctx_callees" => {
1367 let symbol = get_str(args, "symbol")
1368 .ok_or_else(|| ErrorData::invalid_params("symbol is required", None))?;
1369 let file = get_str(args, "file");
1370 let session = self.session.read().await;
1371 let project_root = session
1372 .project_root
1373 .clone()
1374 .unwrap_or_else(|| ".".to_string());
1375 drop(session);
1376 let result =
1377 crate::tools::ctx_callees::handle(&symbol, file.as_deref(), &project_root);
1378 self.record_call("ctx_callees", 0, 0, None).await;
1379 result
1380 }
1381 "ctx_outline" => {
1382 let path = self
1383 .resolve_path(
1384 &get_str(args, "path")
1385 .ok_or_else(|| ErrorData::invalid_params("path is required", None))?,
1386 )
1387 .await
1388 .map_err(|e| ErrorData::invalid_params(e, None))?;
1389 let kind = get_str(args, "kind");
1390 let (result, original) = crate::tools::ctx_outline::handle(&path, kind.as_deref());
1391 let sent = crate::core::tokens::count_tokens(&result);
1392 let saved = original.saturating_sub(sent);
1393 self.record_call("ctx_outline", original, saved, kind).await;
1394 result
1395 }
1396 "ctx_cost" => {
1397 let action = get_str(args, "action").unwrap_or_else(|| "report".to_string());
1398 let agent_id = get_str(args, "agent_id");
1399 let limit = get_int(args, "limit").map(|n| n as usize);
1400 let result = crate::tools::ctx_cost::handle(&action, agent_id.as_deref(), limit);
1401 self.record_call("ctx_cost", 0, 0, Some(action)).await;
1402 result
1403 }
1404 "ctx_discover_tools" => {
1405 let query = get_str(args, "query").unwrap_or_default();
1406 let result = crate::tool_defs::discover_tools(&query);
1407 self.record_call("ctx_discover_tools", 0, 0, None).await;
1408 result
1409 }
1410 "ctx_gain" => {
1411 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
1412 let period = get_str(args, "period");
1413 let model = get_str(args, "model");
1414 let limit = get_int(args, "limit").map(|n| n as usize);
1415 let result = crate::tools::ctx_gain::handle(
1416 &action,
1417 period.as_deref(),
1418 model.as_deref(),
1419 limit,
1420 );
1421 self.record_call("ctx_gain", 0, 0, Some(action)).await;
1422 result
1423 }
1424 "ctx_feedback" => {
1425 let action = get_str(args, "action").unwrap_or_else(|| "report".to_string());
1426 let limit = get_int(args, "limit")
1427 .map(|n| n.max(1) as usize)
1428 .unwrap_or(500);
1429 match action.as_str() {
1430 "record" => {
1431 let current_agent_id = { self.agent_id.read().await.clone() };
1432 let agent_id = get_str(args, "agent_id").or(current_agent_id);
1433 let agent_id = agent_id.ok_or_else(|| {
1434 ErrorData::invalid_params(
1435 "agent_id is required (or register an agent via project_root detection first)",
1436 None,
1437 )
1438 })?;
1439
1440 let (ctx_read_last_mode, ctx_read_modes) = {
1441 let calls = self.tool_calls.read().await;
1442 let mut last: Option<String> = None;
1443 let mut modes: std::collections::BTreeMap<String, u64> =
1444 std::collections::BTreeMap::new();
1445 for rec in calls.iter().rev().take(50) {
1446 if rec.tool != "ctx_read" {
1447 continue;
1448 }
1449 if let Some(m) = rec.mode.as_ref() {
1450 *modes.entry(m.clone()).or_insert(0) += 1;
1451 if last.is_none() {
1452 last = Some(m.clone());
1453 }
1454 }
1455 }
1456 (last, if modes.is_empty() { None } else { Some(modes) })
1457 };
1458
1459 let llm_input_tokens =
1460 get_int(args, "llm_input_tokens").ok_or_else(|| {
1461 ErrorData::invalid_params("llm_input_tokens is required", None)
1462 })?;
1463 let llm_output_tokens =
1464 get_int(args, "llm_output_tokens").ok_or_else(|| {
1465 ErrorData::invalid_params("llm_output_tokens is required", None)
1466 })?;
1467 if llm_input_tokens <= 0 || llm_output_tokens <= 0 {
1468 return Err(ErrorData::invalid_params(
1469 "llm_input_tokens and llm_output_tokens must be > 0",
1470 None,
1471 ));
1472 }
1473
1474 let ev = crate::core::llm_feedback::LlmFeedbackEvent {
1475 agent_id,
1476 intent: get_str(args, "intent"),
1477 model: get_str(args, "model"),
1478 llm_input_tokens: llm_input_tokens as u64,
1479 llm_output_tokens: llm_output_tokens as u64,
1480 latency_ms: get_int(args, "latency_ms").map(|n| n.max(0) as u64),
1481 note: get_str(args, "note"),
1482 ctx_read_last_mode,
1483 ctx_read_modes,
1484 timestamp: chrono::Local::now().to_rfc3339(),
1485 };
1486 let result = crate::tools::ctx_feedback::record(ev)
1487 .unwrap_or_else(|e| format!("Error recording feedback: {e}"));
1488 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1489 result
1490 }
1491 "status" => {
1492 let result = crate::tools::ctx_feedback::status();
1493 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1494 result
1495 }
1496 "json" => {
1497 let result = crate::tools::ctx_feedback::json(limit);
1498 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1499 result
1500 }
1501 "reset" => {
1502 let result = crate::tools::ctx_feedback::reset();
1503 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1504 result
1505 }
1506 _ => {
1507 let result = crate::tools::ctx_feedback::report(limit);
1508 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1509 result
1510 }
1511 }
1512 }
1513 "ctx_handoff" => {
1514 let action = get_str(args, "action").unwrap_or_else(|| "list".to_string());
1515 match action.as_str() {
1516 "list" => {
1517 let items = crate::core::handoff_ledger::list_ledgers();
1518 let result = crate::tools::ctx_handoff::format_list(&items);
1519 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1520 result
1521 }
1522 "clear" => {
1523 let removed =
1524 crate::core::handoff_ledger::clear_ledgers().unwrap_or_default();
1525 let result = crate::tools::ctx_handoff::format_clear(removed);
1526 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1527 result
1528 }
1529 "show" => {
1530 let path = get_str(args, "path").ok_or_else(|| {
1531 ErrorData::invalid_params("path is required for action=show", None)
1532 })?;
1533 let path = self
1534 .resolve_path(&path)
1535 .await
1536 .map_err(|e| ErrorData::invalid_params(e, None))?;
1537 let ledger =
1538 crate::core::handoff_ledger::load_ledger(std::path::Path::new(&path))
1539 .map_err(|e| {
1540 ErrorData::internal_error(format!("load ledger: {e}"), None)
1541 })?;
1542 let result = crate::tools::ctx_handoff::format_show(
1543 std::path::Path::new(&path),
1544 &ledger,
1545 );
1546 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1547 result
1548 }
1549 "create" => {
1550 let curated_paths = get_str_array(args, "paths").unwrap_or_default();
1551 let mut curated_refs: Vec<(String, String)> = Vec::new();
1552 if !curated_paths.is_empty() {
1553 let mut cache = self.cache.write().await;
1554 for p in curated_paths.into_iter().take(20) {
1555 let abs = self
1556 .resolve_path(&p)
1557 .await
1558 .map_err(|e| ErrorData::invalid_params(e, None))?;
1559 let text = crate::tools::ctx_read::handle_with_task(
1560 &mut cache,
1561 &abs,
1562 "signatures",
1563 self.crp_mode,
1564 None,
1565 );
1566 curated_refs.push((abs, text));
1567 }
1568 }
1569
1570 let session = { self.session.read().await.clone() };
1571 let tool_calls = { self.tool_calls.read().await.clone() };
1572 let workflow = { self.workflow.read().await.clone() };
1573 let agent_id = { self.agent_id.read().await.clone() };
1574 let client_name = { self.client_name.read().await.clone() };
1575 let project_root = session.project_root.clone();
1576
1577 let (ledger, path) = crate::core::handoff_ledger::create_ledger(
1578 crate::core::handoff_ledger::CreateLedgerInput {
1579 agent_id,
1580 client_name: Some(client_name),
1581 project_root,
1582 session,
1583 tool_calls,
1584 workflow,
1585 curated_refs,
1586 },
1587 )
1588 .map_err(|e| {
1589 ErrorData::internal_error(format!("create ledger: {e}"), None)
1590 })?;
1591
1592 let result = crate::tools::ctx_handoff::format_created(&path, &ledger);
1593 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1594 result
1595 }
1596 "pull" => {
1597 let path = get_str(args, "path").ok_or_else(|| {
1598 ErrorData::invalid_params("path is required for action=pull", None)
1599 })?;
1600 let path = self
1601 .resolve_path(&path)
1602 .await
1603 .map_err(|e| ErrorData::invalid_params(e, None))?;
1604 let ledger =
1605 crate::core::handoff_ledger::load_ledger(std::path::Path::new(&path))
1606 .map_err(|e| {
1607 ErrorData::internal_error(format!("load ledger: {e}"), None)
1608 })?;
1609
1610 let apply_workflow = get_bool(args, "apply_workflow").unwrap_or(true);
1611 let apply_session = get_bool(args, "apply_session").unwrap_or(true);
1612 let apply_knowledge = get_bool(args, "apply_knowledge").unwrap_or(true);
1613
1614 if apply_workflow {
1615 let mut wf = self.workflow.write().await;
1616 *wf = ledger.workflow.clone();
1617 }
1618
1619 if apply_session {
1620 let mut session = self.session.write().await;
1621 if let Some(t) = ledger.session.task.as_deref() {
1622 session.set_task(t, None);
1623 }
1624 for d in &ledger.session.decisions {
1625 session.add_decision(d, None);
1626 }
1627 for f in &ledger.session.findings {
1628 session.add_finding(None, None, f);
1629 }
1630 session.next_steps = ledger.session.next_steps.clone();
1631 let _ = session.save();
1632 }
1633
1634 let mut knowledge_imported = 0u32;
1635 let mut contradictions = 0u32;
1636 if apply_knowledge {
1637 let root = if let Some(r) = ledger.project_root.as_deref() {
1638 r.to_string()
1639 } else {
1640 let session = self.session.read().await;
1641 session
1642 .project_root
1643 .clone()
1644 .unwrap_or_else(|| ".".to_string())
1645 };
1646 let session_id = {
1647 let s = self.session.read().await;
1648 s.id.clone()
1649 };
1650 let mut knowledge =
1651 crate::core::knowledge::ProjectKnowledge::load_or_create(&root);
1652 for fact in &ledger.knowledge.facts {
1653 let c = knowledge.remember(
1654 &fact.category,
1655 &fact.key,
1656 &fact.value,
1657 &session_id,
1658 fact.confidence,
1659 );
1660 if c.is_some() {
1661 contradictions += 1;
1662 }
1663 knowledge_imported += 1;
1664 }
1665 let _ = knowledge.run_memory_lifecycle();
1666 let _ = knowledge.save();
1667 }
1668
1669 let lines = [
1670 "ctx_handoff pull".to_string(),
1671 format!(" path: {}", path),
1672 format!(" md5: {}", ledger.content_md5),
1673 format!(" applied_workflow: {}", apply_workflow),
1674 format!(" applied_session: {}", apply_session),
1675 format!(" imported_knowledge: {}", knowledge_imported),
1676 format!(" contradictions: {}", contradictions),
1677 ];
1678 let result = lines.join("\n");
1679 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1680 result
1681 }
1682 _ => {
1683 let result =
1684 "Unknown action. Use: create, show, list, pull, clear".to_string();
1685 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1686 result
1687 }
1688 }
1689 }
1690 "ctx_heatmap" => {
1691 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
1692 let path = get_str(args, "path");
1693 let result = crate::tools::ctx_heatmap::handle(&action, path.as_deref());
1694 self.record_call("ctx_heatmap", 0, 0, Some(action)).await;
1695 result
1696 }
1697 "ctx_task" => {
1698 let action = get_str(args, "action").unwrap_or_else(|| "list".to_string());
1699 let current_agent_id = { self.agent_id.read().await.clone() };
1700 let task_id = get_str(args, "task_id");
1701 let to_agent = get_str(args, "to_agent");
1702 let description = get_str(args, "description");
1703 let state = get_str(args, "state");
1704 let message = get_str(args, "message");
1705 let result = crate::tools::ctx_task::handle(
1706 &action,
1707 current_agent_id.as_deref(),
1708 task_id.as_deref(),
1709 to_agent.as_deref(),
1710 description.as_deref(),
1711 state.as_deref(),
1712 message.as_deref(),
1713 );
1714 self.record_call("ctx_task", 0, 0, Some(action)).await;
1715 result
1716 }
1717 "ctx_impact" => {
1718 let action = get_str(args, "action").unwrap_or_else(|| "analyze".to_string());
1719 let path = get_str(args, "path");
1720 let depth = get_int(args, "depth").map(|d| d as usize);
1721 let root = if let Some(r) = get_str(args, "root") {
1722 r
1723 } else {
1724 let session = self.session.read().await;
1725 session
1726 .project_root
1727 .clone()
1728 .unwrap_or_else(|| ".".to_string())
1729 };
1730 let result =
1731 crate::tools::ctx_impact::handle(&action, path.as_deref(), &root, depth);
1732 self.record_call("ctx_impact", 0, 0, Some(action)).await;
1733 result
1734 }
1735 "ctx_architecture" => {
1736 let action = get_str(args, "action").unwrap_or_else(|| "overview".to_string());
1737 let path = get_str(args, "path");
1738 let root = if let Some(r) = get_str(args, "root") {
1739 r
1740 } else {
1741 let session = self.session.read().await;
1742 session
1743 .project_root
1744 .clone()
1745 .unwrap_or_else(|| ".".to_string())
1746 };
1747 let result =
1748 crate::tools::ctx_architecture::handle(&action, path.as_deref(), &root);
1749 self.record_call("ctx_architecture", 0, 0, Some(action))
1750 .await;
1751 result
1752 }
1753 "ctx_workflow" => {
1754 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
1755 let result = {
1756 let mut session = self.session.write().await;
1757 crate::tools::ctx_workflow::handle_with_session(args, &mut session)
1758 };
1759 *self.workflow.write().await = crate::core::workflow::load_active().ok().flatten();
1760 self.record_call("ctx_workflow", 0, 0, Some(action)).await;
1761 result
1762 }
1763 _ => {
1764 return Err(ErrorData::invalid_params(
1765 format!("Unknown tool: {name}"),
1766 None,
1767 ));
1768 }
1769 };
1770
1771 let mut result_text = result_text;
1772
1773 {
1774 let config = crate::core::config::Config::load();
1775 let density = crate::core::config::OutputDensity::effective(&config.output_density);
1776 result_text = crate::core::protocol::compress_output(&result_text, &density);
1777 }
1778
1779 if let Some(ctx) = auto_context {
1780 result_text = format!("{ctx}\n\n{result_text}");
1781 }
1782
1783 if let Some(warning) = throttle_warning {
1784 result_text = format!("{result_text}\n\n{warning}");
1785 }
1786
1787 if name == "ctx_read" {
1788 let read_path = self
1789 .resolve_path_or_passthrough(&get_str(args, "path").unwrap_or_default())
1790 .await;
1791 let project_root = {
1792 let session = self.session.read().await;
1793 session.project_root.clone()
1794 };
1795 let mut cache = self.cache.write().await;
1796 let enrich = crate::tools::autonomy::enrich_after_read(
1797 &self.autonomy,
1798 &mut cache,
1799 &read_path,
1800 project_root.as_deref(),
1801 );
1802 if let Some(hint) = enrich.related_hint {
1803 result_text = format!("{result_text}\n{hint}");
1804 }
1805
1806 crate::tools::autonomy::maybe_auto_dedup(&self.autonomy, &mut cache);
1807 }
1808
1809 if name == "ctx_shell" {
1810 let cmd = get_str(args, "command").unwrap_or_default();
1811 let output_tokens = crate::core::tokens::count_tokens(&result_text);
1812 let calls = self.tool_calls.read().await;
1813 let last_original = calls.last().map(|c| c.original_tokens).unwrap_or(0);
1814 drop(calls);
1815 if let Some(hint) = crate::tools::autonomy::shell_efficiency_hint(
1816 &self.autonomy,
1817 &cmd,
1818 last_original,
1819 output_tokens,
1820 ) {
1821 result_text = format!("{result_text}\n{hint}");
1822 }
1823 }
1824
1825 {
1826 let input = canonical_args_string(args);
1827 let input_md5 = md5_hex(&input);
1828 let output_md5 = md5_hex(&result_text);
1829 let action = get_str(args, "action");
1830 let agent_id = self.agent_id.read().await.clone();
1831 let client_name = self.client_name.read().await.clone();
1832 let mut explicit_intent: Option<(
1833 crate::core::intent_protocol::IntentRecord,
1834 Option<String>,
1835 String,
1836 )> = None;
1837
1838 {
1839 let empty_args = serde_json::Map::new();
1840 let args_map = args.as_ref().unwrap_or(&empty_args);
1841 let mut session = self.session.write().await;
1842 session.record_tool_receipt(
1843 name,
1844 action.as_deref(),
1845 &input_md5,
1846 &output_md5,
1847 agent_id.as_deref(),
1848 Some(&client_name),
1849 );
1850
1851 if let Some(intent) = crate::core::intent_protocol::infer_from_tool_call(
1852 name,
1853 action.as_deref(),
1854 args_map,
1855 session.project_root.as_deref(),
1856 ) {
1857 let is_explicit =
1858 intent.source == crate::core::intent_protocol::IntentSource::Explicit;
1859 let root = session.project_root.clone();
1860 let sid = session.id.clone();
1861 session.record_intent(intent.clone());
1862 if is_explicit {
1863 explicit_intent = Some((intent, root, sid));
1864 }
1865 }
1866 if session.should_save() {
1867 let _ = session.save();
1868 }
1869 }
1870
1871 if let Some((intent, root, session_id)) = explicit_intent {
1872 crate::core::intent_protocol::apply_side_effects(
1873 &intent,
1874 root.as_deref(),
1875 &session_id,
1876 );
1877 }
1878
1879 if self.autonomy.is_enabled() {
1881 let (calls, project_root) = {
1882 let session = self.session.read().await;
1883 (session.stats.total_tool_calls, session.project_root.clone())
1884 };
1885
1886 if let Some(root) = project_root {
1887 if crate::tools::autonomy::should_auto_consolidate(&self.autonomy, calls) {
1888 let root_clone = root.clone();
1889 tokio::task::spawn_blocking(move || {
1890 let _ = crate::core::consolidation_engine::consolidate_latest(
1891 &root_clone,
1892 crate::core::consolidation_engine::ConsolidationBudgets::default(),
1893 );
1894 });
1895 }
1896 }
1897 }
1898
1899 let agent_key = agent_id.unwrap_or_else(|| "unknown".to_string());
1900 let input_tokens = crate::core::tokens::count_tokens(&input) as u64;
1901 let output_tokens = crate::core::tokens::count_tokens(&result_text) as u64;
1902 let mut store = crate::core::a2a::cost_attribution::CostStore::load();
1903 store.record_tool_call(&agent_key, &client_name, name, input_tokens, output_tokens);
1904 let _ = store.save();
1905 }
1906
1907 let skip_checkpoint = matches!(
1908 name,
1909 "ctx_compress"
1910 | "ctx_metrics"
1911 | "ctx_benchmark"
1912 | "ctx_analyze"
1913 | "ctx_cache"
1914 | "ctx_discover"
1915 | "ctx_dedup"
1916 | "ctx_session"
1917 | "ctx_knowledge"
1918 | "ctx_agent"
1919 | "ctx_share"
1920 | "ctx_wrapped"
1921 | "ctx_overview"
1922 | "ctx_preload"
1923 | "ctx_cost"
1924 | "ctx_gain"
1925 | "ctx_heatmap"
1926 | "ctx_task"
1927 | "ctx_impact"
1928 | "ctx_architecture"
1929 | "ctx_workflow"
1930 );
1931
1932 if !skip_checkpoint && self.increment_and_check() {
1933 if let Some(checkpoint) = self.auto_checkpoint().await {
1934 let combined = format!(
1935 "{result_text}\n\n--- AUTO CHECKPOINT (every {} calls) ---\n{checkpoint}",
1936 self.checkpoint_interval
1937 );
1938 return Ok(CallToolResult::success(vec![Content::text(combined)]));
1939 }
1940 }
1941
1942 let tool_duration_ms = tool_start.elapsed().as_millis() as u64;
1943 if tool_duration_ms > 100 {
1944 LeanCtxServer::append_tool_call_log(
1945 name,
1946 tool_duration_ms,
1947 0,
1948 0,
1949 None,
1950 &chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
1951 );
1952 }
1953
1954 let current_count = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
1955 if current_count > 0 && current_count.is_multiple_of(100) {
1956 std::thread::spawn(crate::cloud_sync::cloud_background_tasks);
1957 }
1958
1959 Ok(CallToolResult::success(vec![Content::text(result_text)]))
1960 }
1961}
1962
1963pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
1964 crate::instructions::build_instructions(crp_mode)
1965}
1966
1967pub fn build_claude_code_instructions_for_test() -> String {
1968 crate::instructions::claude_code_instructions()
1969}
1970
1971fn get_str_array(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<Vec<String>> {
1972 let arr = args.as_ref()?.get(key)?.as_array()?;
1973 let mut out = Vec::with_capacity(arr.len());
1974 for v in arr {
1975 let s = v.as_str()?.to_string();
1976 out.push(s);
1977 }
1978 Some(out)
1979}
1980
1981fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
1982 args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
1983}
1984
1985fn get_int(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
1986 args.as_ref()?.get(key)?.as_i64()
1987}
1988
1989fn get_bool(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
1990 args.as_ref()?.get(key)?.as_bool()
1991}
1992
1993fn md5_hex(s: &str) -> String {
1994 let mut hasher = Md5::new();
1995 hasher.update(s.as_bytes());
1996 format!("{:x}", hasher.finalize())
1997}
1998
1999fn canonicalize_json(v: &Value) -> Value {
2000 match v {
2001 Value::Object(map) => {
2002 let mut keys: Vec<&String> = map.keys().collect();
2003 keys.sort();
2004 let mut out = serde_json::Map::new();
2005 for k in keys {
2006 if let Some(val) = map.get(k) {
2007 out.insert(k.clone(), canonicalize_json(val));
2008 }
2009 }
2010 Value::Object(out)
2011 }
2012 Value::Array(arr) => Value::Array(arr.iter().map(canonicalize_json).collect()),
2013 other => other.clone(),
2014 }
2015}
2016
2017fn canonical_args_string(args: &Option<serde_json::Map<String, Value>>) -> String {
2018 let v = args
2019 .as_ref()
2020 .map(|m| Value::Object(m.clone()))
2021 .unwrap_or(Value::Null);
2022 let canon = canonicalize_json(&v);
2023 serde_json::to_string(&canon).unwrap_or_default()
2024}
2025
2026fn extract_search_pattern_from_command(command: &str) -> Option<String> {
2027 let parts: Vec<&str> = command.split_whitespace().collect();
2028 if parts.len() < 2 {
2029 return None;
2030 }
2031 let cmd = parts[0];
2032 if cmd == "grep" || cmd == "rg" || cmd == "ag" || cmd == "ack" {
2033 for (i, part) in parts.iter().enumerate().skip(1) {
2034 if !part.starts_with('-') {
2035 return Some(part.to_string());
2036 }
2037 if (*part == "-e" || *part == "--regexp" || *part == "-m") && i + 1 < parts.len() {
2038 return Some(parts[i + 1].to_string());
2039 }
2040 }
2041 }
2042 if cmd == "find" || cmd == "fd" {
2043 for (i, part) in parts.iter().enumerate() {
2044 if (*part == "-name" || *part == "-iname") && i + 1 < parts.len() {
2045 return Some(
2046 parts[i + 1]
2047 .trim_matches('\'')
2048 .trim_matches('"')
2049 .to_string(),
2050 );
2051 }
2052 }
2053 if cmd == "fd" && parts.len() >= 2 && !parts[1].starts_with('-') {
2054 return Some(parts[1].to_string());
2055 }
2056 }
2057 None
2058}
2059
2060fn execute_command_in(command: &str, cwd: &str) -> (String, i32) {
2061 let (shell, flag) = crate::shell::shell_and_flag();
2062 let normalized_cmd = crate::tools::ctx_shell::normalize_command_for_shell(command);
2063 let dir = std::path::Path::new(cwd);
2064 let mut cmd = std::process::Command::new(&shell);
2065 cmd.arg(&flag)
2066 .arg(&normalized_cmd)
2067 .env("LEAN_CTX_ACTIVE", "1");
2068 if dir.is_dir() {
2069 cmd.current_dir(dir);
2070 }
2071 let cap = crate::core::limits::max_shell_bytes();
2072
2073 fn read_bounded<R: std::io::Read>(mut r: R, cap: usize) -> (Vec<u8>, bool, usize) {
2074 let mut kept: Vec<u8> = Vec::with_capacity(cap.min(8192));
2075 let mut buf = [0u8; 8192];
2076 let mut total = 0usize;
2077 let mut truncated = false;
2078 loop {
2079 match r.read(&mut buf) {
2080 Ok(0) => break,
2081 Ok(n) => {
2082 total = total.saturating_add(n);
2083 if kept.len() < cap {
2084 let remaining = cap - kept.len();
2085 let take = remaining.min(n);
2086 kept.extend_from_slice(&buf[..take]);
2087 if take < n {
2088 truncated = true;
2089 }
2090 } else {
2091 truncated = true;
2092 }
2093 }
2094 Err(_) => break,
2095 }
2096 }
2097 (kept, truncated, total)
2098 }
2099
2100 let mut child = match cmd
2101 .stdout(std::process::Stdio::piped())
2102 .stderr(std::process::Stdio::piped())
2103 .spawn()
2104 {
2105 Ok(c) => c,
2106 Err(e) => return (format!("ERROR: {e}"), 1),
2107 };
2108 let stdout = child.stdout.take();
2109 let stderr = child.stderr.take();
2110
2111 let out_handle = std::thread::spawn(move || {
2112 stdout
2113 .map(|s| read_bounded(s, cap))
2114 .unwrap_or_else(|| (Vec::new(), false, 0))
2115 });
2116 let err_handle = std::thread::spawn(move || {
2117 stderr
2118 .map(|s| read_bounded(s, cap))
2119 .unwrap_or_else(|| (Vec::new(), false, 0))
2120 });
2121
2122 let status = child.wait();
2123 let code = status.ok().and_then(|s| s.code()).unwrap_or(1);
2124
2125 let (out_bytes, out_trunc, _out_total) = out_handle.join().unwrap_or_default();
2126 let (err_bytes, err_trunc, _err_total) = err_handle.join().unwrap_or_default();
2127
2128 let stdout = String::from_utf8_lossy(&out_bytes);
2129 let stderr = String::from_utf8_lossy(&err_bytes);
2130 let mut text = if stdout.is_empty() {
2131 stderr.to_string()
2132 } else if stderr.is_empty() {
2133 stdout.to_string()
2134 } else {
2135 format!("{stdout}\n{stderr}")
2136 };
2137
2138 if out_trunc || err_trunc {
2139 text.push_str(&format!(
2140 "\n[truncated: cap={}B stdout={}B stderr={}B]",
2141 cap,
2142 out_bytes.len(),
2143 err_bytes.len()
2144 ));
2145 }
2146
2147 (text, code)
2148}
2149
2150const PROJECT_MARKERS: &[&str] = &[
2151 ".git",
2152 "Cargo.toml",
2153 "package.json",
2154 "go.mod",
2155 "pyproject.toml",
2156 "setup.py",
2157 "pom.xml",
2158 "build.gradle",
2159 "Makefile",
2160 ".lean-ctx.toml",
2161];
2162
2163fn has_project_marker(dir: &std::path::Path) -> bool {
2164 PROJECT_MARKERS.iter().any(|m| dir.join(m).exists())
2165}
2166
2167fn is_home_or_agent_dir(dir: &std::path::Path) -> bool {
2168 if let Some(home) = dirs::home_dir() {
2169 if dir == home {
2170 return true;
2171 }
2172 }
2173 let dir_str = dir.to_string_lossy();
2174 dir_str.ends_with("/.claude")
2175 || dir_str.ends_with("/.codex")
2176 || dir_str.contains("/.claude/")
2177 || dir_str.contains("/.codex/")
2178}
2179
2180fn git_toplevel_from(dir: &std::path::Path) -> Option<String> {
2181 std::process::Command::new("git")
2182 .args(["rev-parse", "--show-toplevel"])
2183 .current_dir(dir)
2184 .stdout(std::process::Stdio::piped())
2185 .stderr(std::process::Stdio::null())
2186 .output()
2187 .ok()
2188 .and_then(|o| {
2189 if o.status.success() {
2190 String::from_utf8(o.stdout)
2191 .ok()
2192 .map(|s| s.trim().to_string())
2193 } else {
2194 None
2195 }
2196 })
2197}
2198
2199pub fn derive_project_root_from_cwd() -> Option<String> {
2200 let cwd = std::env::current_dir().ok()?;
2201 let canonical = crate::core::pathutil::safe_canonicalize_or_self(&cwd);
2202
2203 if is_home_or_agent_dir(&canonical) {
2204 return git_toplevel_from(&canonical);
2205 }
2206
2207 if has_project_marker(&canonical) {
2208 return Some(canonical.to_string_lossy().to_string());
2209 }
2210
2211 if let Some(git_root) = git_toplevel_from(&canonical) {
2212 return Some(git_root);
2213 }
2214
2215 None
2216}
2217
2218pub fn tool_descriptions_for_test() -> Vec<(&'static str, &'static str)> {
2219 crate::tool_defs::list_all_tool_defs()
2220 .into_iter()
2221 .map(|(name, desc, _)| (name, desc))
2222 .collect()
2223}
2224
2225pub fn tool_schemas_json_for_test() -> String {
2226 crate::tool_defs::list_all_tool_defs()
2227 .iter()
2228 .map(|(name, _, schema)| format!("{}: {}", name, schema))
2229 .collect::<Vec<_>>()
2230 .join("\n")
2231}
2232
2233#[cfg(test)]
2234mod tests {
2235 use super::*;
2236
2237 #[test]
2238 fn project_markers_detected() {
2239 let tmp = tempfile::tempdir().unwrap();
2240 let root = tmp.path().join("myproject");
2241 std::fs::create_dir_all(&root).unwrap();
2242 assert!(!has_project_marker(&root));
2243
2244 std::fs::create_dir(root.join(".git")).unwrap();
2245 assert!(has_project_marker(&root));
2246 }
2247
2248 #[test]
2249 fn home_dir_detected_as_agent_dir() {
2250 if let Some(home) = dirs::home_dir() {
2251 assert!(is_home_or_agent_dir(&home));
2252 }
2253 }
2254
2255 #[test]
2256 fn agent_dirs_detected() {
2257 let claude = std::path::PathBuf::from("/home/user/.claude");
2258 assert!(is_home_or_agent_dir(&claude));
2259 let codex = std::path::PathBuf::from("/home/user/.codex");
2260 assert!(is_home_or_agent_dir(&codex));
2261 let project = std::path::PathBuf::from("/home/user/projects/myapp");
2262 assert!(!is_home_or_agent_dir(&project));
2263 }
2264
2265 #[test]
2266 fn test_unified_tool_count() {
2267 let tools = crate::tool_defs::unified_tool_defs();
2268 assert_eq!(tools.len(), 5, "Expected 5 unified tools");
2269 }
2270
2271 #[test]
2272 fn test_granular_tool_count() {
2273 let tools = crate::tool_defs::granular_tool_defs();
2274 assert!(tools.len() >= 25, "Expected at least 25 granular tools");
2275 }
2276
2277 #[test]
2278 fn disabled_tools_filters_list() {
2279 let all = crate::tool_defs::granular_tool_defs();
2280 let total = all.len();
2281 let disabled = ["ctx_graph".to_string(), "ctx_agent".to_string()];
2282 let filtered: Vec<_> = all
2283 .into_iter()
2284 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
2285 .collect();
2286 assert_eq!(filtered.len(), total - 2);
2287 assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_graph"));
2288 assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_agent"));
2289 }
2290
2291 #[test]
2292 fn empty_disabled_tools_returns_all() {
2293 let all = crate::tool_defs::granular_tool_defs();
2294 let total = all.len();
2295 let disabled: Vec<String> = vec![];
2296 let filtered: Vec<_> = all
2297 .into_iter()
2298 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
2299 .collect();
2300 assert_eq!(filtered.len(), total);
2301 }
2302
2303 #[test]
2304 fn misspelled_disabled_tool_is_silently_ignored() {
2305 let all = crate::tool_defs::granular_tool_defs();
2306 let total = all.len();
2307 let disabled = ["ctx_nonexistent_tool".to_string()];
2308 let filtered: Vec<_> = all
2309 .into_iter()
2310 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
2311 .collect();
2312 assert_eq!(filtered.len(), total);
2313 }
2314}