Skip to main content

lean_ctx/
server.rs

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