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