Skip to main content

lean_ctx/
server.rs

1use std::sync::Arc;
2
3use rmcp::handler::server::ServerHandler;
4use rmcp::model::*;
5use rmcp::service::{RequestContext, RoleServer};
6use rmcp::ErrorData;
7use serde_json::{json, Map, Value};
8
9use crate::tools::{CrpMode, LeanCtxServer};
10
11// Unified mode is opt-in only via LEAN_CTX_UNIFIED env var.
12// Granular tools (25 individual ctx_* tools) are the default for all clients.
13
14impl ServerHandler for LeanCtxServer {
15    fn get_info(&self) -> ServerInfo {
16        let capabilities = ServerCapabilities::builder().enable_tools().build();
17
18        let instructions = build_instructions(self.crp_mode);
19
20        InitializeResult::new(capabilities)
21            .with_server_info(Implementation::new("lean-ctx", "2.16.6"))
22            .with_instructions(instructions)
23    }
24
25    async fn initialize(
26        &self,
27        request: InitializeRequestParams,
28        _context: RequestContext<RoleServer>,
29    ) -> Result<InitializeResult, ErrorData> {
30        let name = request.client_info.name.clone();
31        tracing::info!("MCP client connected: {:?}", name);
32        *self.client_name.write().await = name.clone();
33
34        tokio::task::spawn_blocking(|| {
35            if let Some(home) = dirs::home_dir() {
36                let _ = crate::rules_inject::inject_all_rules(&home);
37            }
38            crate::hooks::refresh_installed_hooks();
39            crate::core::version_check::check_background();
40        });
41
42        let instructions = build_instructions_with_client(self.crp_mode, &name);
43        let capabilities = ServerCapabilities::builder().enable_tools().build();
44
45        Ok(InitializeResult::new(capabilities)
46            .with_server_info(Implementation::new("lean-ctx", "2.16.6"))
47            .with_instructions(instructions))
48    }
49
50    async fn list_tools(
51        &self,
52        _request: Option<PaginatedRequestParams>,
53        _context: RequestContext<RoleServer>,
54    ) -> Result<ListToolsResult, ErrorData> {
55        if should_use_unified(&self.client_name.read().await) {
56            return Ok(ListToolsResult {
57                tools: unified_tool_defs(),
58                ..Default::default()
59            });
60        }
61
62        Ok(ListToolsResult {
63                tools: vec![
64                    tool_def(
65                        "ctx_read",
66                        "Read file (cached, compressed). Re-reads ~13 tok. Auto-selects optimal mode. \
67Modes: full|map|signatures|diff|aggressive|entropy|task|reference|lines:N-M. fresh=true re-reads.",
68                        json!({
69                            "type": "object",
70                            "properties": {
71                                "path": { "type": "string", "description": "Absolute file path to read" },
72                                "mode": {
73                                    "type": "string",
74                                    "description": "Compression mode (default: full). Use 'map' for context-only files. For line ranges: 'lines:N-M' (e.g. 'lines:400-500')."
75                                },
76                                "start_line": {
77                                    "type": "integer",
78                                    "description": "Read from this line number to end of file. Bypasses cache stub — always returns actual content."
79                                },
80                                "fresh": {
81                                    "type": "boolean",
82                                    "description": "Bypass cache and force a full re-read. Use when running as a subagent that may not have the parent's context."
83                                }
84                            },
85                            "required": ["path"]
86                        }),
87                    ),
88                    tool_def(
89                        "ctx_multi_read",
90                        "Batch read files in one call. Same modes as ctx_read.",
91                        json!({
92                            "type": "object",
93                            "properties": {
94                                "paths": {
95                                    "type": "array",
96                                    "items": { "type": "string" },
97                                    "description": "Absolute file paths to read, in order"
98                                },
99                                "mode": {
100                                    "type": "string",
101                                    "enum": ["full", "signatures", "map", "diff", "aggressive", "entropy"],
102                                    "description": "Compression mode (default: full)"
103                                }
104                            },
105                            "required": ["paths"]
106                        }),
107                    ),
108                    tool_def(
109                        "ctx_tree",
110                        "Directory listing with file counts.",
111                        json!({
112                            "type": "object",
113                            "properties": {
114                                "path": { "type": "string", "description": "Directory path (default: .)" },
115                                "depth": { "type": "integer", "description": "Max depth (default: 3)" },
116                                "show_hidden": { "type": "boolean", "description": "Show hidden files" }
117                            }
118                        }),
119                    ),
120                    tool_def(
121                        "ctx_shell",
122                        "Run shell command (compressed output, 90+ patterns). Use raw=true to skip compression and get full output.",
123                        json!({
124                            "type": "object",
125                            "properties": {
126                                "command": { "type": "string", "description": "Shell command to execute" },
127                                "raw": { "type": "boolean", "description": "Skip compression, return full uncompressed output. Use for small outputs or when full detail is critical." }
128                            },
129                            "required": ["command"]
130                        }),
131                    ),
132                    tool_def(
133                        "ctx_search",
134                        "Regex code search (.gitignore aware, compact results).",
135                        json!({
136                            "type": "object",
137                            "properties": {
138                                "pattern": { "type": "string", "description": "Regex pattern" },
139                                "path": { "type": "string", "description": "Directory to search" },
140                                "ext": { "type": "string", "description": "File extension filter" },
141                                "max_results": { "type": "integer", "description": "Max results (default: 20)" },
142                                "ignore_gitignore": { "type": "boolean", "description": "Set true to scan ALL files including .gitignore'd paths (default: false)" }
143                            },
144                            "required": ["pattern"]
145                        }),
146                    ),
147                    tool_def(
148                        "ctx_compress",
149                        "Context checkpoint for long conversations.",
150                        json!({
151                            "type": "object",
152                            "properties": {
153                                "include_signatures": { "type": "boolean", "description": "Include signatures (default: true)" }
154                            }
155                        }),
156                    ),
157                    tool_def(
158                        "ctx_benchmark",
159                        "Benchmark compression modes for a file or project.",
160                        json!({
161                            "type": "object",
162                            "properties": {
163                                "path": { "type": "string", "description": "File path (action=file) or project directory (action=project)" },
164                                "action": { "type": "string", "description": "file (default) or project", "default": "file" },
165                                "format": { "type": "string", "description": "Output format for project benchmark: terminal, markdown, json", "default": "terminal" }
166                            },
167                            "required": ["path"]
168                        }),
169                    ),
170                    tool_def(
171                        "ctx_metrics",
172                        "Session token stats, cache rates, per-tool savings.",
173                        json!({
174                            "type": "object",
175                            "properties": {}
176                        }),
177                    ),
178                    tool_def(
179                        "ctx_analyze",
180                        "Entropy analysis — recommends optimal compression mode for a file.",
181                        json!({
182                            "type": "object",
183                            "properties": {
184                                "path": { "type": "string", "description": "File path to analyze" }
185                            },
186                            "required": ["path"]
187                        }),
188                    ),
189                    tool_def(
190                        "ctx_cache",
191                        "Cache ops: status|clear|invalidate.",
192                        json!({
193                            "type": "object",
194                            "properties": {
195                                "action": {
196                                    "type": "string",
197                                    "enum": ["status", "clear", "invalidate"],
198                                    "description": "Cache operation to perform"
199                                },
200                                "path": {
201                                    "type": "string",
202                                    "description": "File path (required for 'invalidate' action)"
203                                }
204                            },
205                            "required": ["action"]
206                        }),
207                    ),
208                    tool_def(
209                        "ctx_discover",
210                        "Find missed compression opportunities in shell history.",
211                        json!({
212                            "type": "object",
213                            "properties": {
214                                "limit": {
215                                    "type": "integer",
216                                    "description": "Max number of command types to show (default: 15)"
217                                }
218                            }
219                        }),
220                    ),
221                    tool_def(
222                        "ctx_smart_read",
223                        "Auto-select optimal read mode for a file.",
224                        json!({
225                            "type": "object",
226                            "properties": {
227                                "path": { "type": "string", "description": "Absolute file path to read" }
228                            },
229                            "required": ["path"]
230                        }),
231                    ),
232                    tool_def(
233                        "ctx_delta",
234                        "Incremental diff — sends only changed lines since last read.",
235                        json!({
236                            "type": "object",
237                            "properties": {
238                                "path": { "type": "string", "description": "Absolute file path" }
239                            },
240                            "required": ["path"]
241                        }),
242                    ),
243                    tool_def(
244                        "ctx_edit",
245                        "Edit a file via search-and-replace. Works without native Read/Edit tools. Use this when the IDE's Edit tool requires Read but Read is unavailable.",
246                        json!({
247                            "type": "object",
248                            "properties": {
249                                "path": { "type": "string", "description": "Absolute file path" },
250                                "old_string": { "type": "string", "description": "Exact text to find and replace (must be unique unless replace_all=true)" },
251                                "new_string": { "type": "string", "description": "Replacement text" },
252                                "replace_all": { "type": "boolean", "description": "Replace all occurrences (default: false)", "default": false },
253                                "create": { "type": "boolean", "description": "Create a new file with new_string as content (ignores old_string)", "default": false }
254                            },
255                            "required": ["path", "new_string"]
256                        }),
257                    ),
258                    tool_def(
259                        "ctx_dedup",
260                        "Cross-file dedup: analyze or apply shared block references.",
261                        json!({
262                            "type": "object",
263                            "properties": {
264                                "action": {
265                                    "type": "string",
266                                    "description": "analyze (default) or apply (register shared blocks for auto-dedup in ctx_read)",
267                                    "default": "analyze"
268                                }
269                            }
270                        }),
271                    ),
272                    tool_def(
273                        "ctx_fill",
274                        "Budget-aware context fill — auto-selects compression per file within token limit.",
275                        json!({
276                            "type": "object",
277                            "properties": {
278                                "paths": {
279                                    "type": "array",
280                                    "items": { "type": "string" },
281                                    "description": "File paths to consider"
282                                },
283                                "budget": {
284                                    "type": "integer",
285                                    "description": "Maximum token budget to fill"
286                                }
287                            },
288                            "required": ["paths", "budget"]
289                        }),
290                    ),
291                    tool_def(
292                        "ctx_intent",
293                        "Intent detection — auto-reads relevant files based on task description.",
294                        json!({
295                            "type": "object",
296                            "properties": {
297                                "query": { "type": "string", "description": "Natural language description of the task" },
298                                "project_root": { "type": "string", "description": "Project root directory (default: .)" }
299                            },
300                            "required": ["query"]
301                        }),
302                    ),
303                    tool_def(
304                        "ctx_response",
305                        "Compress LLM response text (remove filler, apply TDD).",
306                        json!({
307                            "type": "object",
308                            "properties": {
309                                "text": { "type": "string", "description": "Response text to compress" }
310                            },
311                            "required": ["text"]
312                        }),
313                    ),
314                    tool_def(
315                        "ctx_context",
316                        "Session context overview — cached files, seen files, session state.",
317                        json!({
318                            "type": "object",
319                            "properties": {}
320                        }),
321                    ),
322                    tool_def(
323                        "ctx_graph",
324                        "Code dependency graph. Actions: build (index project), related (find files connected to path), \
325symbol (lookup definition/usages as file::name), impact (blast radius of changes to path), status (index stats).",
326                        json!({
327                            "type": "object",
328                            "properties": {
329                                "action": {
330                                    "type": "string",
331                                    "enum": ["build", "related", "symbol", "impact", "status"],
332                                    "description": "Graph operation: build, related, symbol, impact, status"
333                                },
334                                "path": {
335                                    "type": "string",
336                                    "description": "File path (related/impact) or file::symbol_name (symbol)"
337                                },
338                                "project_root": {
339                                    "type": "string",
340                                    "description": "Project root directory (default: .)"
341                                }
342                            },
343                            "required": ["action"]
344                        }),
345                    ),
346                    tool_def(
347                        "ctx_session",
348                        "Cross-session memory (CCP). Actions: load (restore previous session ~400 tok), \
349save, status, task (set current task), finding (record discovery), decision (record choice), \
350reset, list (show sessions), cleanup.",
351                        json!({
352                            "type": "object",
353                            "properties": {
354                                "action": {
355                                    "type": "string",
356                                    "enum": ["status", "load", "save", "task", "finding", "decision", "reset", "list", "cleanup"],
357                                    "description": "Session operation to perform"
358                                },
359                                "value": {
360                                    "type": "string",
361                                    "description": "Value for task/finding/decision actions"
362                                },
363                                "session_id": {
364                                    "type": "string",
365                                    "description": "Session ID for load action (default: latest)"
366                                }
367                            },
368                            "required": ["action"]
369                        }),
370                    ),
371                    tool_def(
372                        "ctx_knowledge",
373                        "Persistent project knowledge (survives sessions). Actions: remember (store fact with category+key+value), \
374recall (search by query), pattern (record naming/structure pattern), consolidate (extract session findings into knowledge), \
375status (list all), remove, export.",
376                        json!({
377                            "type": "object",
378                            "properties": {
379                                "action": {
380                                    "type": "string",
381                                    "enum": ["remember", "recall", "pattern", "consolidate", "status", "remove", "export"],
382                                    "description": "Knowledge operation to perform"
383                                },
384                                "category": {
385                                    "type": "string",
386                                    "description": "Fact category (architecture, api, testing, deployment, conventions, dependencies)"
387                                },
388                                "key": {
389                                    "type": "string",
390                                    "description": "Fact key/identifier (e.g. 'auth-method', 'db-engine', 'test-framework')"
391                                },
392                                "value": {
393                                    "type": "string",
394                                    "description": "Fact value or pattern description"
395                                },
396                                "query": {
397                                    "type": "string",
398                                    "description": "Search query for recall action (matches against category, key, and value)"
399                                },
400                                "pattern_type": {
401                                    "type": "string",
402                                    "description": "Pattern type for pattern action (naming, structure, testing, error-handling)"
403                                },
404                                "examples": {
405                                    "type": "array",
406                                    "items": { "type": "string" },
407                                    "description": "Examples for pattern action"
408                                },
409                                "confidence": {
410                                    "type": "number",
411                                    "description": "Confidence score 0.0-1.0 for remember action (default: 0.8)"
412                                }
413                            },
414                            "required": ["action"]
415                        }),
416                    ),
417                    tool_def(
418                        "ctx_agent",
419                        "Multi-agent coordination (shared message bus). Actions: register (join with agent_type+role), \
420post (broadcast or direct message with category), read (poll messages), status (update state: active|idle|finished), \
421list, info.",
422                        json!({
423                            "type": "object",
424                            "properties": {
425                                "action": {
426                                    "type": "string",
427                                    "enum": ["register", "list", "post", "read", "status", "info"],
428                                    "description": "Agent operation to perform"
429                                },
430                                "agent_type": {
431                                    "type": "string",
432                                    "description": "Agent type for register (cursor, claude, codex, gemini, subagent)"
433                                },
434                                "role": {
435                                    "type": "string",
436                                    "description": "Agent role (dev, review, test, plan)"
437                                },
438                                "message": {
439                                    "type": "string",
440                                    "description": "Message text for post action, or status detail for status action"
441                                },
442                                "category": {
443                                    "type": "string",
444                                    "description": "Message category for post (finding, warning, request, status)"
445                                },
446                                "to_agent": {
447                                    "type": "string",
448                                    "description": "Target agent ID for direct message (omit for broadcast)"
449                                },
450                                "status": {
451                                    "type": "string",
452                                    "enum": ["active", "idle", "finished"],
453                                    "description": "New status for status action"
454                                }
455                            },
456                            "required": ["action"]
457                        }),
458                    ),
459                    tool_def(
460                        "ctx_overview",
461                        "Task-relevant project map — use at session start.",
462                        json!({
463                            "type": "object",
464                            "properties": {
465                                "task": {
466                                    "type": "string",
467                                    "description": "Task description for relevance scoring (e.g. 'fix auth bug in login flow')"
468                                },
469                                "path": {
470                                    "type": "string",
471                                    "description": "Project root directory (default: .)"
472                                }
473                            }
474                        }),
475                    ),
476                    tool_def(
477                        "ctx_preload",
478                        "Proactive context loader — caches task-relevant files, returns L-curve-optimized summary (~50-100 tokens vs ~5000 for individual reads).",
479                        json!({
480                            "type": "object",
481                            "properties": {
482                                "task": {
483                                    "type": "string",
484                                    "description": "Task description (e.g. 'fix auth bug in validate_token')"
485                                },
486                                "path": {
487                                    "type": "string",
488                                    "description": "Project root (default: .)"
489                                }
490                            },
491                            "required": ["task"]
492                        }),
493                    ),
494                    tool_def(
495                        "ctx_wrapped",
496                        "Savings report card. Periods: week|month|all.",
497                        json!({
498                            "type": "object",
499                            "properties": {
500                                "period": {
501                                    "type": "string",
502                                    "enum": ["week", "month", "all"],
503                                    "description": "Report period (default: week)"
504                                }
505                            }
506                        }),
507                    ),
508                    tool_def(
509                        "ctx_semantic_search",
510                        "BM25 code search by meaning. action=reindex to rebuild.",
511                        json!({
512                            "type": "object",
513                            "properties": {
514                                "query": { "type": "string", "description": "Natural language search query" },
515                                "path": { "type": "string", "description": "Project root to search (default: .)" },
516                                "top_k": { "type": "integer", "description": "Number of results (default: 10)" },
517                                "action": { "type": "string", "description": "reindex to rebuild index" }
518                            },
519                            "required": ["query"]
520                        }),
521                    ),
522                ],
523                ..Default::default()
524            })
525    }
526
527    async fn call_tool(
528        &self,
529        request: CallToolRequestParams,
530        _context: RequestContext<RoleServer>,
531    ) -> Result<CallToolResult, ErrorData> {
532        self.check_idle_expiry().await;
533
534        let original_name = request.name.as_ref().to_string();
535        let (resolved_name, resolved_args) = if original_name == "ctx" {
536            let sub = request
537                .arguments
538                .as_ref()
539                .and_then(|a| a.get("tool"))
540                .and_then(|v| v.as_str())
541                .map(|s| s.to_string())
542                .ok_or_else(|| {
543                    ErrorData::invalid_params("'tool' is required for ctx meta-tool", None)
544                })?;
545            let tool_name = if sub.starts_with("ctx_") {
546                sub
547            } else {
548                format!("ctx_{sub}")
549            };
550            let mut args = request.arguments.unwrap_or_default();
551            args.remove("tool");
552            (tool_name, Some(args))
553        } else {
554            (original_name, request.arguments)
555        };
556        let name = resolved_name.as_str();
557        let args = &resolved_args;
558
559        let auto_context = {
560            let task = {
561                let session = self.session.read().await;
562                session.task.as_ref().map(|t| t.description.clone())
563            };
564            let project_root = {
565                let session = self.session.read().await;
566                session.project_root.clone()
567            };
568            let mut cache = self.cache.write().await;
569            crate::tools::autonomy::session_lifecycle_pre_hook(
570                &self.autonomy,
571                name,
572                &mut cache,
573                task.as_deref(),
574                project_root.as_deref(),
575                self.crp_mode,
576            )
577        };
578
579        let tool_start = std::time::Instant::now();
580        let result_text = match name {
581            "ctx_read" => {
582                let path = get_str(args, "path")
583                    .map(|p| crate::hooks::normalize_tool_path(&p))
584                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
585                let current_task = {
586                    let session = self.session.read().await;
587                    session.task.as_ref().map(|t| t.description.clone())
588                };
589                let task_ref = current_task.as_deref();
590                let mut mode = match get_str(args, "mode") {
591                    Some(m) => m,
592                    None => {
593                        let cache = self.cache.read().await;
594                        crate::tools::ctx_smart_read::select_mode_with_task(&cache, &path, task_ref)
595                    }
596                };
597                let fresh = get_bool(args, "fresh").unwrap_or(false);
598                let start_line = get_int(args, "start_line");
599                if let Some(sl) = start_line {
600                    let sl = sl.max(1_i64);
601                    mode = format!("lines:{sl}-999999");
602                }
603                let stale = self.is_prompt_cache_stale().await;
604                let effective_mode = LeanCtxServer::upgrade_mode_if_stale(&mode, stale).to_string();
605                let mut cache = self.cache.write().await;
606                let output = if fresh {
607                    crate::tools::ctx_read::handle_fresh_with_task(
608                        &mut cache,
609                        &path,
610                        &effective_mode,
611                        self.crp_mode,
612                        task_ref,
613                    )
614                } else {
615                    crate::tools::ctx_read::handle_with_task(
616                        &mut cache,
617                        &path,
618                        &effective_mode,
619                        self.crp_mode,
620                        task_ref,
621                    )
622                };
623                let stale_note = if effective_mode != mode {
624                    format!("[cache stale, {mode}→{effective_mode}]\n")
625                } else {
626                    String::new()
627                };
628                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
629                let output_tokens = crate::core::tokens::count_tokens(&output);
630                let saved = original.saturating_sub(output_tokens);
631                let is_cache_hit = output.contains(" cached ");
632                let output = format!("{stale_note}{output}");
633                let file_ref = cache.file_ref_map().get(&path).cloned();
634                drop(cache);
635                {
636                    let mut session = self.session.write().await;
637                    session.touch_file(&path, file_ref.as_deref(), &effective_mode, original);
638                    if is_cache_hit {
639                        session.record_cache_hit();
640                    }
641                    if session.project_root.is_none() {
642                        if let Some(root) = detect_project_root(&path) {
643                            session.project_root = Some(root.clone());
644                            let mut current = self.agent_id.write().await;
645                            if current.is_none() {
646                                let mut registry =
647                                    crate::core::agents::AgentRegistry::load_or_create();
648                                registry.cleanup_stale(24);
649                                let id = registry.register("mcp", None, &root);
650                                let _ = registry.save();
651                                *current = Some(id);
652                            }
653                        }
654                    }
655                }
656                self.record_call("ctx_read", original, saved, Some(mode.clone()))
657                    .await;
658                {
659                    let sig =
660                        crate::core::mode_predictor::FileSignature::from_path(&path, original);
661                    let density = if output_tokens > 0 {
662                        original as f64 / output_tokens as f64
663                    } else {
664                        1.0
665                    };
666                    let outcome = crate::core::mode_predictor::ModeOutcome {
667                        mode: mode.clone(),
668                        tokens_in: original,
669                        tokens_out: output_tokens,
670                        density: density.min(1.0),
671                    };
672                    let mut predictor = crate::core::mode_predictor::ModePredictor::new();
673                    predictor.record(sig, outcome);
674                    predictor.save();
675
676                    let ext = std::path::Path::new(&path)
677                        .extension()
678                        .and_then(|e| e.to_str())
679                        .unwrap_or("")
680                        .to_string();
681                    let thresholds = crate::core::adaptive_thresholds::thresholds_for_path(&path);
682                    let cache = self.cache.read().await;
683                    let stats = cache.get_stats();
684                    let feedback_outcome = crate::core::feedback::CompressionOutcome {
685                        session_id: format!("{}", std::process::id()),
686                        language: ext,
687                        entropy_threshold: thresholds.bpe_entropy,
688                        jaccard_threshold: thresholds.jaccard,
689                        total_turns: stats.total_reads as u32,
690                        tokens_saved: saved as u64,
691                        tokens_original: original as u64,
692                        cache_hits: stats.cache_hits as u32,
693                        total_reads: stats.total_reads as u32,
694                        task_completed: true,
695                        timestamp: chrono::Local::now().to_rfc3339(),
696                    };
697                    drop(cache);
698                    let mut store = crate::core::feedback::FeedbackStore::load();
699                    store.record_outcome(feedback_outcome);
700                }
701                output
702            }
703            "ctx_multi_read" => {
704                let paths = get_str_array(args, "paths")
705                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?
706                    .into_iter()
707                    .map(|p| crate::hooks::normalize_tool_path(&p))
708                    .collect::<Vec<_>>();
709                let mode = get_str(args, "mode").unwrap_or_else(|| "full".to_string());
710                let current_task = {
711                    let session = self.session.read().await;
712                    session.task.as_ref().map(|t| t.description.clone())
713                };
714                let mut cache = self.cache.write().await;
715                let output = crate::tools::ctx_multi_read::handle_with_task(
716                    &mut cache,
717                    &paths,
718                    &mode,
719                    self.crp_mode,
720                    current_task.as_deref(),
721                );
722                let mut total_original: usize = 0;
723                for path in &paths {
724                    total_original = total_original
725                        .saturating_add(cache.get(path).map(|e| e.original_tokens).unwrap_or(0));
726                }
727                let tokens = crate::core::tokens::count_tokens(&output);
728                drop(cache);
729                self.record_call(
730                    "ctx_multi_read",
731                    total_original,
732                    total_original.saturating_sub(tokens),
733                    Some(mode),
734                )
735                .await;
736                output
737            }
738            "ctx_tree" => {
739                let path = crate::hooks::normalize_tool_path(
740                    &get_str(args, "path").unwrap_or_else(|| ".".to_string()),
741                );
742                let depth = get_int(args, "depth").unwrap_or(3) as usize;
743                let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
744                let (result, original) = crate::tools::ctx_tree::handle(&path, depth, show_hidden);
745                let sent = crate::core::tokens::count_tokens(&result);
746                let saved = original.saturating_sub(sent);
747                self.record_call("ctx_tree", original, saved, None).await;
748                let savings_note = if saved > 0 {
749                    format!("\n[saved {saved} tokens vs native ls]")
750                } else {
751                    String::new()
752                };
753                format!("{result}{savings_note}")
754            }
755            "ctx_shell" => {
756                let command = get_str(args, "command")
757                    .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
758                let raw = get_bool(args, "raw").unwrap_or(false)
759                    || std::env::var("LEAN_CTX_DISABLED").is_ok();
760                let output = execute_command(&command);
761
762                if raw {
763                    let original = crate::core::tokens::count_tokens(&output);
764                    self.record_call("ctx_shell", original, 0, None).await;
765                    output
766                } else {
767                    let result = crate::tools::ctx_shell::handle(&command, &output, self.crp_mode);
768                    let original = crate::core::tokens::count_tokens(&output);
769                    let sent = crate::core::tokens::count_tokens(&result);
770                    let saved = original.saturating_sub(sent);
771                    self.record_call("ctx_shell", original, saved, None).await;
772
773                    let cfg = crate::core::config::Config::load();
774                    let tee_hint = match cfg.tee_mode {
775                        crate::core::config::TeeMode::Always => {
776                            crate::shell::save_tee(&command, &output)
777                                .map(|p| format!("\n[full output: {p}]"))
778                                .unwrap_or_default()
779                        }
780                        crate::core::config::TeeMode::Failures
781                            if !output.trim().is_empty() && output.contains("error")
782                                || output.contains("Error")
783                                || output.contains("ERROR") =>
784                        {
785                            crate::shell::save_tee(&command, &output)
786                                .map(|p| format!("\n[full output: {p}]"))
787                                .unwrap_or_default()
788                        }
789                        _ => String::new(),
790                    };
791
792                    let savings_note = if saved > 0 {
793                        format!("\n[saved {saved} tokens vs native Shell]")
794                    } else {
795                        String::new()
796                    };
797                    format!("{result}{savings_note}{tee_hint}")
798                }
799            }
800            "ctx_search" => {
801                let pattern = get_str(args, "pattern")
802                    .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
803                let path = crate::hooks::normalize_tool_path(
804                    &get_str(args, "path").unwrap_or_else(|| ".".to_string()),
805                );
806                let ext = get_str(args, "ext");
807                let max = get_int(args, "max_results").unwrap_or(20) as usize;
808                let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
809                let crp = self.crp_mode;
810                let respect = !no_gitignore;
811                let search_result = tokio::time::timeout(
812                    std::time::Duration::from_secs(30),
813                    tokio::task::spawn_blocking(move || {
814                        crate::tools::ctx_search::handle(
815                            &pattern,
816                            &path,
817                            ext.as_deref(),
818                            max,
819                            crp,
820                            respect,
821                        )
822                    }),
823                )
824                .await;
825                let (result, original) = match search_result {
826                    Ok(Ok(r)) => r,
827                    Ok(Err(e)) => {
828                        return Err(ErrorData::internal_error(
829                            format!("search task failed: {e}"),
830                            None,
831                        ))
832                    }
833                    Err(_) => {
834                        let msg = "ctx_search timed out after 30s. Try narrowing the search:\n\
835                                   • Use a more specific pattern\n\
836                                   • Specify ext= to limit file types\n\
837                                   • Specify a subdirectory in path=";
838                        self.record_call("ctx_search", 0, 0, None).await;
839                        return Ok(CallToolResult::success(vec![Content::text(msg)]));
840                    }
841                };
842                let sent = crate::core::tokens::count_tokens(&result);
843                let saved = original.saturating_sub(sent);
844                self.record_call("ctx_search", original, saved, None).await;
845                let savings_note = if saved > 0 {
846                    format!("\n[saved {saved} tokens vs native Grep]")
847                } else {
848                    String::new()
849                };
850                format!("{result}{savings_note}")
851            }
852            "ctx_compress" => {
853                let include_sigs = get_bool(args, "include_signatures").unwrap_or(true);
854                let cache = self.cache.read().await;
855                let result =
856                    crate::tools::ctx_compress::handle(&cache, include_sigs, self.crp_mode);
857                drop(cache);
858                self.record_call("ctx_compress", 0, 0, None).await;
859                result
860            }
861            "ctx_benchmark" => {
862                let path = get_str(args, "path")
863                    .map(|p| crate::hooks::normalize_tool_path(&p))
864                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
865                let action = get_str(args, "action").unwrap_or_default();
866                let result = if action == "project" {
867                    let fmt = get_str(args, "format").unwrap_or_default();
868                    let bench = crate::core::benchmark::run_project_benchmark(&path);
869                    match fmt.as_str() {
870                        "json" => crate::core::benchmark::format_json(&bench),
871                        "markdown" | "md" => crate::core::benchmark::format_markdown(&bench),
872                        _ => crate::core::benchmark::format_terminal(&bench),
873                    }
874                } else {
875                    crate::tools::ctx_benchmark::handle(&path, self.crp_mode)
876                };
877                self.record_call("ctx_benchmark", 0, 0, None).await;
878                result
879            }
880            "ctx_metrics" => {
881                let cache = self.cache.read().await;
882                let calls = self.tool_calls.read().await;
883                let result = crate::tools::ctx_metrics::handle(&cache, &calls, self.crp_mode);
884                drop(cache);
885                drop(calls);
886                self.record_call("ctx_metrics", 0, 0, None).await;
887                result
888            }
889            "ctx_analyze" => {
890                let path = get_str(args, "path")
891                    .map(|p| crate::hooks::normalize_tool_path(&p))
892                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
893                let result = crate::tools::ctx_analyze::handle(&path, self.crp_mode);
894                self.record_call("ctx_analyze", 0, 0, None).await;
895                result
896            }
897            "ctx_discover" => {
898                let limit = get_int(args, "limit").unwrap_or(15) as usize;
899                let history = crate::cli::load_shell_history_pub();
900                let result = crate::tools::ctx_discover::discover_from_history(&history, limit);
901                self.record_call("ctx_discover", 0, 0, None).await;
902                result
903            }
904            "ctx_smart_read" => {
905                let path = get_str(args, "path")
906                    .map(|p| crate::hooks::normalize_tool_path(&p))
907                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
908                let mut cache = self.cache.write().await;
909                let output = crate::tools::ctx_smart_read::handle(&mut cache, &path, self.crp_mode);
910                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
911                let tokens = crate::core::tokens::count_tokens(&output);
912                drop(cache);
913                self.record_call(
914                    "ctx_smart_read",
915                    original,
916                    original.saturating_sub(tokens),
917                    Some("auto".to_string()),
918                )
919                .await;
920                output
921            }
922            "ctx_delta" => {
923                let path = get_str(args, "path")
924                    .map(|p| crate::hooks::normalize_tool_path(&p))
925                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
926                let mut cache = self.cache.write().await;
927                let output = crate::tools::ctx_delta::handle(&mut cache, &path);
928                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
929                let tokens = crate::core::tokens::count_tokens(&output);
930                drop(cache);
931                {
932                    let mut session = self.session.write().await;
933                    session.mark_modified(&path);
934                }
935                self.record_call(
936                    "ctx_delta",
937                    original,
938                    original.saturating_sub(tokens),
939                    Some("delta".to_string()),
940                )
941                .await;
942                output
943            }
944            "ctx_edit" => {
945                let path = get_str(args, "path")
946                    .map(|p| crate::hooks::normalize_tool_path(&p))
947                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
948                let old_string = get_str(args, "old_string").unwrap_or_default();
949                let new_string = get_str(args, "new_string")
950                    .ok_or_else(|| ErrorData::invalid_params("new_string is required", None))?;
951                let replace_all = args
952                    .as_ref()
953                    .and_then(|a| a.get("replace_all"))
954                    .and_then(|v| v.as_bool())
955                    .unwrap_or(false);
956                let create = args
957                    .as_ref()
958                    .and_then(|a| a.get("create"))
959                    .and_then(|v| v.as_bool())
960                    .unwrap_or(false);
961
962                let mut cache = self.cache.write().await;
963                let output = crate::tools::ctx_edit::handle(
964                    &mut cache,
965                    crate::tools::ctx_edit::EditParams {
966                        path: path.clone(),
967                        old_string,
968                        new_string,
969                        replace_all,
970                        create,
971                    },
972                );
973                drop(cache);
974
975                {
976                    let mut session = self.session.write().await;
977                    session.mark_modified(&path);
978                }
979                self.record_call("ctx_edit", 0, 0, None).await;
980                output
981            }
982            "ctx_dedup" => {
983                let action = get_str(args, "action").unwrap_or_default();
984                if action == "apply" {
985                    let mut cache = self.cache.write().await;
986                    let result = crate::tools::ctx_dedup::handle_action(&mut cache, &action);
987                    drop(cache);
988                    self.record_call("ctx_dedup", 0, 0, None).await;
989                    result
990                } else {
991                    let cache = self.cache.read().await;
992                    let result = crate::tools::ctx_dedup::handle(&cache);
993                    drop(cache);
994                    self.record_call("ctx_dedup", 0, 0, None).await;
995                    result
996                }
997            }
998            "ctx_fill" => {
999                let paths = get_str_array(args, "paths")
1000                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?
1001                    .into_iter()
1002                    .map(|p| crate::hooks::normalize_tool_path(&p))
1003                    .collect::<Vec<_>>();
1004                let budget = get_int(args, "budget")
1005                    .ok_or_else(|| ErrorData::invalid_params("budget is required", None))?
1006                    as usize;
1007                let mut cache = self.cache.write().await;
1008                let output =
1009                    crate::tools::ctx_fill::handle(&mut cache, &paths, budget, self.crp_mode);
1010                drop(cache);
1011                self.record_call("ctx_fill", 0, 0, Some(format!("budget:{budget}")))
1012                    .await;
1013                output
1014            }
1015            "ctx_intent" => {
1016                let query = get_str(args, "query")
1017                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
1018                let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
1019                let mut cache = self.cache.write().await;
1020                let output =
1021                    crate::tools::ctx_intent::handle(&mut cache, &query, &root, self.crp_mode);
1022                drop(cache);
1023                {
1024                    let mut session = self.session.write().await;
1025                    session.set_task(&query, Some("intent"));
1026                }
1027                self.record_call("ctx_intent", 0, 0, Some("semantic".to_string()))
1028                    .await;
1029                output
1030            }
1031            "ctx_response" => {
1032                let text = get_str(args, "text")
1033                    .ok_or_else(|| ErrorData::invalid_params("text is required", None))?;
1034                let output = crate::tools::ctx_response::handle(&text, self.crp_mode);
1035                self.record_call("ctx_response", 0, 0, None).await;
1036                output
1037            }
1038            "ctx_context" => {
1039                let cache = self.cache.read().await;
1040                let turn = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
1041                let result = crate::tools::ctx_context::handle_status(&cache, turn, self.crp_mode);
1042                drop(cache);
1043                self.record_call("ctx_context", 0, 0, None).await;
1044                result
1045            }
1046            "ctx_graph" => {
1047                let action = get_str(args, "action")
1048                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1049                let path = get_str(args, "path").map(|p| crate::hooks::normalize_tool_path(&p));
1050                let root = crate::hooks::normalize_tool_path(
1051                    &get_str(args, "project_root").unwrap_or_else(|| ".".to_string()),
1052                );
1053                let mut cache = self.cache.write().await;
1054                let result = crate::tools::ctx_graph::handle(
1055                    &action,
1056                    path.as_deref(),
1057                    &root,
1058                    &mut cache,
1059                    self.crp_mode,
1060                );
1061                drop(cache);
1062                self.record_call("ctx_graph", 0, 0, Some(action)).await;
1063                result
1064            }
1065            "ctx_cache" => {
1066                let action = get_str(args, "action")
1067                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1068                let mut cache = self.cache.write().await;
1069                let result = match action.as_str() {
1070                    "status" => {
1071                        let entries = cache.get_all_entries();
1072                        if entries.is_empty() {
1073                            "Cache empty — no files tracked.".to_string()
1074                        } else {
1075                            let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
1076                            for (path, entry) in &entries {
1077                                let fref = cache
1078                                    .file_ref_map()
1079                                    .get(*path)
1080                                    .map(|s| s.as_str())
1081                                    .unwrap_or("F?");
1082                                lines.push(format!(
1083                                    "  {fref}={} [{}L, {}t, read {}x]",
1084                                    crate::core::protocol::shorten_path(path),
1085                                    entry.line_count,
1086                                    entry.original_tokens,
1087                                    entry.read_count
1088                                ));
1089                            }
1090                            lines.join("\n")
1091                        }
1092                    }
1093                    "clear" => {
1094                        let count = cache.clear();
1095                        format!("Cache cleared — {count} file(s) removed. Next ctx_read will return full content.")
1096                    }
1097                    "invalidate" => {
1098                        let path = get_str(args, "path")
1099                            .map(|p| crate::hooks::normalize_tool_path(&p))
1100                            .ok_or_else(|| {
1101                                ErrorData::invalid_params("path is required for invalidate", None)
1102                            })?;
1103                        if cache.invalidate(&path) {
1104                            format!(
1105                                "Invalidated cache for {}. Next ctx_read will return full content.",
1106                                crate::core::protocol::shorten_path(&path)
1107                            )
1108                        } else {
1109                            format!(
1110                                "{} was not in cache.",
1111                                crate::core::protocol::shorten_path(&path)
1112                            )
1113                        }
1114                    }
1115                    _ => "Unknown action. Use: status, clear, invalidate".to_string(),
1116                };
1117                drop(cache);
1118                self.record_call("ctx_cache", 0, 0, Some(action)).await;
1119                result
1120            }
1121            "ctx_session" => {
1122                let action = get_str(args, "action")
1123                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1124                let value = get_str(args, "value");
1125                let sid = get_str(args, "session_id");
1126                let mut session = self.session.write().await;
1127                let result = crate::tools::ctx_session::handle(
1128                    &mut session,
1129                    &action,
1130                    value.as_deref(),
1131                    sid.as_deref(),
1132                );
1133                drop(session);
1134                self.record_call("ctx_session", 0, 0, Some(action)).await;
1135                result
1136            }
1137            "ctx_knowledge" => {
1138                let action = get_str(args, "action")
1139                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1140                let category = get_str(args, "category");
1141                let key = get_str(args, "key");
1142                let value = get_str(args, "value");
1143                let query = get_str(args, "query");
1144                let pattern_type = get_str(args, "pattern_type");
1145                let examples = get_str_array(args, "examples");
1146                let confidence: Option<f32> = args
1147                    .as_ref()
1148                    .and_then(|a| a.get("confidence"))
1149                    .and_then(|v| v.as_f64())
1150                    .map(|v| v as f32);
1151
1152                let session = self.session.read().await;
1153                let session_id = session.id.clone();
1154                let project_root = session.project_root.clone().unwrap_or_else(|| {
1155                    std::env::current_dir()
1156                        .map(|p| p.to_string_lossy().to_string())
1157                        .unwrap_or_else(|_| "unknown".to_string())
1158                });
1159                drop(session);
1160
1161                let result = crate::tools::ctx_knowledge::handle(
1162                    &project_root,
1163                    &action,
1164                    category.as_deref(),
1165                    key.as_deref(),
1166                    value.as_deref(),
1167                    query.as_deref(),
1168                    &session_id,
1169                    pattern_type.as_deref(),
1170                    examples,
1171                    confidence,
1172                );
1173                self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
1174                result
1175            }
1176            "ctx_agent" => {
1177                let action = get_str(args, "action")
1178                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1179                let agent_type = get_str(args, "agent_type");
1180                let role = get_str(args, "role");
1181                let message = get_str(args, "message");
1182                let category = get_str(args, "category");
1183                let to_agent = get_str(args, "to_agent");
1184                let status = get_str(args, "status");
1185
1186                let session = self.session.read().await;
1187                let project_root = session.project_root.clone().unwrap_or_else(|| {
1188                    std::env::current_dir()
1189                        .map(|p| p.to_string_lossy().to_string())
1190                        .unwrap_or_else(|_| "unknown".to_string())
1191                });
1192                drop(session);
1193
1194                let current_agent_id = self.agent_id.read().await.clone();
1195                let result = crate::tools::ctx_agent::handle(
1196                    &action,
1197                    agent_type.as_deref(),
1198                    role.as_deref(),
1199                    &project_root,
1200                    current_agent_id.as_deref(),
1201                    message.as_deref(),
1202                    category.as_deref(),
1203                    to_agent.as_deref(),
1204                    status.as_deref(),
1205                );
1206
1207                if action == "register" {
1208                    if let Some(id) = result.split(':').nth(1) {
1209                        let id = id.split_whitespace().next().unwrap_or("").to_string();
1210                        if !id.is_empty() {
1211                            *self.agent_id.write().await = Some(id);
1212                        }
1213                    }
1214                }
1215
1216                self.record_call("ctx_agent", 0, 0, Some(action)).await;
1217                result
1218            }
1219            "ctx_overview" => {
1220                let task = get_str(args, "task");
1221                let path = get_str(args, "path").map(|p| crate::hooks::normalize_tool_path(&p));
1222                let cache = self.cache.read().await;
1223                let result = crate::tools::ctx_overview::handle(
1224                    &cache,
1225                    task.as_deref(),
1226                    path.as_deref(),
1227                    self.crp_mode,
1228                );
1229                drop(cache);
1230                self.record_call("ctx_overview", 0, 0, Some("overview".to_string()))
1231                    .await;
1232                result
1233            }
1234            "ctx_preload" => {
1235                let task = get_str(args, "task").unwrap_or_default();
1236                let path = get_str(args, "path").map(|p| crate::hooks::normalize_tool_path(&p));
1237                let mut cache = self.cache.write().await;
1238                let result = crate::tools::ctx_preload::handle(
1239                    &mut cache,
1240                    &task,
1241                    path.as_deref(),
1242                    self.crp_mode,
1243                );
1244                drop(cache);
1245                self.record_call("ctx_preload", 0, 0, Some("preload".to_string()))
1246                    .await;
1247                result
1248            }
1249            "ctx_wrapped" => {
1250                let period = get_str(args, "period").unwrap_or_else(|| "week".to_string());
1251                let result = crate::tools::ctx_wrapped::handle(&period);
1252                self.record_call("ctx_wrapped", 0, 0, Some(period)).await;
1253                result
1254            }
1255            "ctx_semantic_search" => {
1256                let query = get_str(args, "query")
1257                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
1258                let path = crate::hooks::normalize_tool_path(
1259                    &get_str(args, "path").unwrap_or_else(|| ".".to_string()),
1260                );
1261                let top_k = get_int(args, "top_k").unwrap_or(10) as usize;
1262                let action = get_str(args, "action").unwrap_or_default();
1263                let result = if action == "reindex" {
1264                    crate::tools::ctx_semantic_search::handle_reindex(&path)
1265                } else {
1266                    crate::tools::ctx_semantic_search::handle(&query, &path, top_k, self.crp_mode)
1267                };
1268                self.record_call("ctx_semantic_search", 0, 0, Some("semantic".to_string()))
1269                    .await;
1270                result
1271            }
1272            _ => {
1273                return Err(ErrorData::invalid_params(
1274                    format!("Unknown tool: {name}"),
1275                    None,
1276                ));
1277            }
1278        };
1279
1280        let mut result_text = result_text;
1281
1282        if let Some(ctx) = auto_context {
1283            result_text = format!("{ctx}\n\n{result_text}");
1284        }
1285
1286        if name == "ctx_read" {
1287            let read_path =
1288                crate::hooks::normalize_tool_path(&get_str(args, "path").unwrap_or_default());
1289            let project_root = {
1290                let session = self.session.read().await;
1291                session.project_root.clone()
1292            };
1293            let mut cache = self.cache.write().await;
1294            let enrich = crate::tools::autonomy::enrich_after_read(
1295                &self.autonomy,
1296                &mut cache,
1297                &read_path,
1298                project_root.as_deref(),
1299            );
1300            if let Some(hint) = enrich.related_hint {
1301                result_text = format!("{result_text}\n{hint}");
1302            }
1303
1304            crate::tools::autonomy::maybe_auto_dedup(&self.autonomy, &mut cache);
1305        }
1306
1307        if name == "ctx_shell" {
1308            let cmd = get_str(args, "command").unwrap_or_default();
1309            let output_tokens = crate::core::tokens::count_tokens(&result_text);
1310            let calls = self.tool_calls.read().await;
1311            let last_original = calls.last().map(|c| c.original_tokens).unwrap_or(0);
1312            drop(calls);
1313            if let Some(hint) = crate::tools::autonomy::shell_efficiency_hint(
1314                &self.autonomy,
1315                &cmd,
1316                last_original,
1317                output_tokens,
1318            ) {
1319                result_text = format!("{result_text}\n{hint}");
1320            }
1321        }
1322
1323        let skip_checkpoint = matches!(
1324            name,
1325            "ctx_compress"
1326                | "ctx_metrics"
1327                | "ctx_benchmark"
1328                | "ctx_analyze"
1329                | "ctx_cache"
1330                | "ctx_discover"
1331                | "ctx_dedup"
1332                | "ctx_session"
1333                | "ctx_knowledge"
1334                | "ctx_agent"
1335                | "ctx_wrapped"
1336                | "ctx_overview"
1337                | "ctx_preload"
1338        );
1339
1340        if !skip_checkpoint && self.increment_and_check() {
1341            if let Some(checkpoint) = self.auto_checkpoint().await {
1342                let combined = format!(
1343                    "{result_text}\n\n--- AUTO CHECKPOINT (every {} calls) ---\n{checkpoint}",
1344                    self.checkpoint_interval
1345                );
1346                return Ok(CallToolResult::success(vec![Content::text(combined)]));
1347            }
1348        }
1349
1350        let tool_duration_ms = tool_start.elapsed().as_millis() as u64;
1351        if tool_duration_ms > 100 {
1352            LeanCtxServer::append_tool_call_log(
1353                name,
1354                tool_duration_ms,
1355                0,
1356                0,
1357                None,
1358                &chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
1359            );
1360        }
1361
1362        let current_count = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
1363        if current_count > 0 && current_count.is_multiple_of(100) {
1364            std::thread::spawn(cloud_background_tasks);
1365        }
1366
1367        Ok(CallToolResult::success(vec![Content::text(result_text)]))
1368    }
1369}
1370
1371fn build_instructions(crp_mode: CrpMode) -> String {
1372    build_instructions_with_client(crp_mode, "")
1373}
1374
1375fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
1376    let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
1377    let session_block = match crate::core::session::SessionState::load_latest() {
1378        Some(ref session) => {
1379            let positioned = crate::core::litm::position_optimize(session);
1380            format!(
1381                "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
1382                profile.name, positioned.begin_block
1383            )
1384        }
1385        None => String::new(),
1386    };
1387
1388    let knowledge_block = {
1389        let project_root = crate::core::session::SessionState::load_latest()
1390            .and_then(|s| s.project_root)
1391            .or_else(|| {
1392                std::env::current_dir()
1393                    .ok()
1394                    .map(|p| p.to_string_lossy().to_string())
1395            });
1396        match project_root {
1397            Some(root) => {
1398                let knowledge = crate::core::knowledge::ProjectKnowledge::load(&root);
1399                match knowledge {
1400                    Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
1401                        format!("\n--- PROJECT KNOWLEDGE ---\n{}\n---\n", k.format_summary())
1402                    }
1403                    _ => String::new(),
1404                }
1405            }
1406            None => String::new(),
1407        }
1408    };
1409
1410    // Prefix-cache alignment: stable instructions first (API providers cache KV states
1411    // for shared prefixes), then variable session state after.
1412    let mut base = format!("\
1413PREFER lean-ctx tools over native equivalents for token savings:\n\
1414\n\
1415lean-ctx MCP — tool mapping:\n\
1416• Read/cat/head/tail -> ctx_read(path, mode)\n\
1417• Shell/bash -> ctx_shell(command)\n\
1418• Grep/rg -> ctx_search(pattern, path)\n\
1419• ls/find -> ctx_tree(path, depth)\n\
1420• Edit/StrReplace -> use native if available, otherwise use ctx_edit(path, old_string, new_string)\n\
1421• Write, Delete, Glob -> use normally\n\
1422\n\
1423FILE EDITING: Use your IDE's native Edit/StrReplace when available. \
1424If Edit requires native Read and Read is unavailable, use ctx_edit instead — it reads, replaces, and writes in one call. \
1425NEVER loop trying to make Edit work. If Edit fails, switch to ctx_edit immediately.\n\
1426\n\
1427ctx_read modes: full (cached, for edits), map (deps+API), signatures, diff, task (IB-filtered), \
1428reference, aggressive, entropy, lines:N-M. Auto-selects when unspecified. Re-reads ~13 tokens. File refs F1,F2.. persist.\n\
1429If ctx_read returns 'cached': use fresh=true, start_line=N, or mode='lines:N-M' to re-read.\n\
1430\n\
1431AUTONOMY: lean-ctx auto-runs ctx_overview, ctx_preload, ctx_dedup, ctx_compress behind the scenes.\n\
1432Focus on: ctx_read, ctx_shell, ctx_search, ctx_tree. Use ctx_session for memory, ctx_knowledge for project facts.\n\
1433ctx_shell raw=true: skip compression for small/critical outputs. Full output tee files at ~/.lean-ctx/tee/.\n\
1434\n\
1435Auto-checkpoint every 15 calls. Cache clears after 5 min idle.\n\
1436\n\
1437CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) 4.ONE LINE PER ACTION 5.QUALITY ANCHOR\n\
1438\n\
1439{decoder_block}\n\
1440\n\
1441{session_block}\
1442{knowledge_block}\
1443\n\
1444--- TOOL PREFERENCE (LITM-END) ---\n\
1445Prefer: ctx_read over Read | ctx_shell over Shell | ctx_search over Grep | ctx_tree over ls\n\
1446Edit files: native Edit/StrReplace if available, ctx_edit if Edit requires unavailable Read.\n\
1447Write, Delete, Glob -> use normally. NEVER loop on Edit failures — use ctx_edit.",
1448        decoder_block = crate::core::protocol::instruction_decoder_block()
1449    );
1450
1451    if should_use_unified(client_name) {
1452        base.push_str(
1453            "\n\n\
1454UNIFIED TOOL MODE (active):\n\
1455Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
1456See the ctx() tool description for available sub-tools.\n",
1457        );
1458    }
1459
1460    let intelligence_block = build_intelligence_block();
1461
1462    let base = base;
1463    match crp_mode {
1464        CrpMode::Off => format!("{base}\n\n{intelligence_block}"),
1465        CrpMode::Compact => {
1466            format!(
1467                "{base}\n\n\
1468CRP MODE: compact\n\
1469Compact Response Protocol:\n\
1470• Omit filler words, articles, redundant phrases\n\
1471• Abbreviate: fn, cfg, impl, deps, req, res, ctx, err, ret, arg, val, ty, mod\n\
1472• Compact lists over prose, code blocks over explanations\n\
1473• Code changes: diff lines (+/-) only, not full files\n\
1474• TARGET: <=200 tokens per response unless code edits require more\n\
1475• Tool outputs are pre-analyzed and compressed. Trust them directly.\n\n\
1476{intelligence_block}"
1477            )
1478        }
1479        CrpMode::Tdd => {
1480            format!(
1481                "{base}\n\n\
1482CRP MODE: tdd (Token Dense Dialect)\n\
1483Maximize information density. Every token must carry meaning.\n\
1484\n\
1485RESPONSE RULES:\n\
1486• Drop articles, filler words, pleasantries\n\
1487• Reference files by Fn refs only, never full paths\n\
1488• Code changes: diff lines only (+/-), not full files\n\
1489• No explanations unless asked\n\
1490• Tables for structured data\n\
1491• Abbreviations: fn, cfg, impl, deps, req, res, ctx, err, ret, arg, val, ty, mod\n\
1492\n\
1493CHANGE NOTATION:\n\
1494+F1:42 param(timeout:Duration)     — added\n\
1495-F1:10-15                           — removed\n\
1496~F1:42 validate_token -> verify_jwt — changed\n\
1497\n\
1498STATUS: ctx_read(F1) -> 808L cached ok | cargo test -> 82 passed 0 failed\n\
1499\n\
1500TOKEN BUDGET: <=150 tokens per response. Exceed only for multi-file edits.\n\
1501Tool outputs are pre-analyzed and compressed. Trust them directly.\n\
1502ZERO NARRATION: Act, then report result in 1 line.\n\n\
1503{intelligence_block}"
1504            )
1505        }
1506    }
1507}
1508
1509fn build_intelligence_block() -> String {
1510    "\
1511OUTPUT EFFICIENCY:\n\
1512• NEVER echo back code that was provided in tool outputs — it wastes tokens.\n\
1513• NEVER add narration comments (// Import, // Define, // Return) — code is self-documenting.\n\
1514• For code changes: show only the new/changed code, not unchanged context.\n\
1515• Tool outputs include [TASK:type] and SCOPE hints for context.\n\
1516• Respect the user's intent: architecture tasks need thorough analysis, simple generates need code."
1517        .to_string()
1518}
1519
1520fn tool_def(name: &'static str, description: &'static str, schema_value: Value) -> Tool {
1521    let schema: Map<String, Value> = match schema_value {
1522        Value::Object(map) => map,
1523        _ => Map::new(),
1524    };
1525    Tool::new(name, description, Arc::new(schema))
1526}
1527
1528fn unified_tool_defs() -> Vec<Tool> {
1529    vec![
1530        tool_def(
1531            "ctx_read",
1532            "Read file (cached, compressed). Modes: full|map|signatures|diff|aggressive|entropy|task|reference|lines:N-M. fresh=true re-reads.",
1533            json!({
1534                "type": "object",
1535                "properties": {
1536                    "path": { "type": "string", "description": "File path" },
1537                    "mode": { "type": "string" },
1538                    "start_line": { "type": "integer" },
1539                    "fresh": { "type": "boolean" }
1540                },
1541                "required": ["path"]
1542            }),
1543        ),
1544        tool_def(
1545            "ctx_shell",
1546            "Run shell command (compressed output). raw=true skips compression.",
1547            json!({
1548                "type": "object",
1549                "properties": {
1550                    "command": { "type": "string", "description": "Shell command" },
1551                    "raw": { "type": "boolean", "description": "Skip compression for full output" }
1552                },
1553                "required": ["command"]
1554            }),
1555        ),
1556        tool_def(
1557            "ctx_search",
1558            "Regex code search (.gitignore aware).",
1559            json!({
1560                "type": "object",
1561                "properties": {
1562                    "pattern": { "type": "string", "description": "Regex pattern" },
1563                    "path": { "type": "string" },
1564                    "ext": { "type": "string" },
1565                    "max_results": { "type": "integer" },
1566                    "ignore_gitignore": { "type": "boolean" }
1567                },
1568                "required": ["pattern"]
1569            }),
1570        ),
1571        tool_def(
1572            "ctx_tree",
1573            "Directory listing with file counts.",
1574            json!({
1575                "type": "object",
1576                "properties": {
1577                    "path": { "type": "string" },
1578                    "depth": { "type": "integer" },
1579                    "show_hidden": { "type": "boolean" }
1580                }
1581            }),
1582        ),
1583        tool_def(
1584            "ctx",
1585            "Meta-tool: set tool= to sub-tool name. Sub-tools: compress (checkpoint), metrics (stats), \
1586analyze (entropy), cache (status|clear|invalidate), discover (missed patterns), smart_read (auto-mode), \
1587delta (incremental diff), dedup (cross-file), fill (budget-aware batch read), intent (auto-read by task), \
1588response (compress LLM text), context (session state), graph (build|related|symbol|impact|status), \
1589session (load|save|task|finding|decision|status|reset|list|cleanup), \
1590knowledge (remember|recall|pattern|consolidate|status|remove|export), \
1591agent (register|post|read|status|list|info), overview (project map), \
1592wrapped (savings report), benchmark (file|project), multi_read (batch), semantic_search (BM25).",
1593            json!({
1594                "type": "object",
1595                "properties": {
1596                    "tool": {
1597                        "type": "string",
1598                        "description": "compress|metrics|analyze|cache|discover|smart_read|delta|dedup|fill|intent|response|context|graph|session|knowledge|agent|overview|wrapped|benchmark|multi_read|semantic_search"
1599                    },
1600                    "action": { "type": "string" },
1601                    "path": { "type": "string" },
1602                    "paths": { "type": "array", "items": { "type": "string" } },
1603                    "query": { "type": "string" },
1604                    "value": { "type": "string" },
1605                    "category": { "type": "string" },
1606                    "key": { "type": "string" },
1607                    "budget": { "type": "integer" },
1608                    "task": { "type": "string" },
1609                    "mode": { "type": "string" },
1610                    "text": { "type": "string" },
1611                    "message": { "type": "string" },
1612                    "session_id": { "type": "string" },
1613                    "period": { "type": "string" },
1614                    "format": { "type": "string" },
1615                    "agent_type": { "type": "string" },
1616                    "role": { "type": "string" },
1617                    "status": { "type": "string" },
1618                    "pattern_type": { "type": "string" },
1619                    "examples": { "type": "array", "items": { "type": "string" } },
1620                    "confidence": { "type": "number" },
1621                    "project_root": { "type": "string" },
1622                    "include_signatures": { "type": "boolean" },
1623                    "limit": { "type": "integer" },
1624                    "to_agent": { "type": "string" },
1625                    "show_hidden": { "type": "boolean" }
1626                },
1627                "required": ["tool"]
1628            }),
1629        ),
1630    ]
1631}
1632
1633fn should_use_unified(client_name: &str) -> bool {
1634    if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
1635        return false;
1636    }
1637    if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
1638        return true;
1639    }
1640    let _ = client_name;
1641    false
1642}
1643
1644fn get_str_array(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<Vec<String>> {
1645    let arr = args.as_ref()?.get(key)?.as_array()?;
1646    let mut out = Vec::with_capacity(arr.len());
1647    for v in arr {
1648        let s = v.as_str()?.to_string();
1649        out.push(s);
1650    }
1651    Some(out)
1652}
1653
1654fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
1655    args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
1656}
1657
1658fn get_int(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
1659    args.as_ref()?.get(key)?.as_i64()
1660}
1661
1662fn get_bool(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
1663    args.as_ref()?.get(key)?.as_bool()
1664}
1665
1666fn execute_command(command: &str) -> String {
1667    let (shell, flag) = crate::shell::shell_and_flag();
1668    let output = std::process::Command::new(&shell)
1669        .arg(&flag)
1670        .arg(command)
1671        .env("LEAN_CTX_ACTIVE", "1")
1672        .output();
1673
1674    match output {
1675        Ok(out) => {
1676            let stdout = String::from_utf8_lossy(&out.stdout);
1677            let stderr = String::from_utf8_lossy(&out.stderr);
1678            if stdout.is_empty() {
1679                stderr.to_string()
1680            } else if stderr.is_empty() {
1681                stdout.to_string()
1682            } else {
1683                format!("{stdout}\n{stderr}")
1684            }
1685        }
1686        Err(e) => format!("ERROR: {e}"),
1687    }
1688}
1689
1690fn detect_project_root(file_path: &str) -> Option<String> {
1691    let mut dir = std::path::Path::new(file_path).parent()?;
1692    loop {
1693        if dir.join(".git").exists() {
1694            return Some(dir.to_string_lossy().to_string());
1695        }
1696        dir = dir.parent()?;
1697    }
1698}
1699
1700fn cloud_background_tasks() {
1701    use crate::core::config::Config;
1702
1703    let mut config = Config::load();
1704    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
1705
1706    let already_contributed = config
1707        .cloud
1708        .last_contribute
1709        .as_deref()
1710        .map(|d| d == today)
1711        .unwrap_or(false);
1712    let already_synced = config
1713        .cloud
1714        .last_sync
1715        .as_deref()
1716        .map(|d| d == today)
1717        .unwrap_or(false);
1718    let already_pulled = config
1719        .cloud
1720        .last_model_pull
1721        .as_deref()
1722        .map(|d| d == today)
1723        .unwrap_or(false);
1724
1725    if config.cloud.contribute_enabled && !already_contributed {
1726        if let Some(home) = dirs::home_dir() {
1727            let mode_stats_path = home.join(".lean-ctx").join("mode_stats.json");
1728            if let Ok(data) = std::fs::read_to_string(&mode_stats_path) {
1729                if let Ok(predictor) = serde_json::from_str::<serde_json::Value>(&data) {
1730                    let mut entries = Vec::new();
1731                    if let Some(history) = predictor["history"].as_object() {
1732                        for (_key, outcomes) in history {
1733                            if let Some(arr) = outcomes.as_array() {
1734                                for outcome in arr.iter().rev().take(3) {
1735                                    let ext = outcome["ext"].as_str().unwrap_or("unknown");
1736                                    let mode = outcome["mode"].as_str().unwrap_or("full");
1737                                    let t_in = outcome["tokens_in"].as_u64().unwrap_or(0);
1738                                    let t_out = outcome["tokens_out"].as_u64().unwrap_or(0);
1739                                    let ratio = if t_in > 0 {
1740                                        1.0 - t_out as f64 / t_in as f64
1741                                    } else {
1742                                        0.0
1743                                    };
1744                                    let bucket = match t_in {
1745                                        0..=500 => "0-500",
1746                                        501..=2000 => "500-2k",
1747                                        2001..=10000 => "2k-10k",
1748                                        _ => "10k+",
1749                                    };
1750                                    entries.push(serde_json::json!({
1751                                        "file_ext": format!(".{ext}"),
1752                                        "size_bucket": bucket,
1753                                        "best_mode": mode,
1754                                        "compression_ratio": (ratio * 100.0).round() / 100.0,
1755                                    }));
1756                                    if entries.len() >= 200 {
1757                                        break;
1758                                    }
1759                                }
1760                            }
1761                            if entries.len() >= 200 {
1762                                break;
1763                            }
1764                        }
1765                    }
1766                    if !entries.is_empty() && crate::cloud_client::contribute(&entries).is_ok() {
1767                        config.cloud.last_contribute = Some(today.clone());
1768                    }
1769                }
1770            }
1771        }
1772    }
1773
1774    if crate::cloud_client::check_pro() {
1775        if !already_synced {
1776            let stats_data = crate::core::stats::format_gain_json();
1777            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stats_data) {
1778                let entry = serde_json::json!({
1779                    "date": &today,
1780                    "tokens_original": parsed["total_original_tokens"].as_i64().unwrap_or(0),
1781                    "tokens_compressed": parsed["total_compressed_tokens"].as_i64().unwrap_or(0),
1782                    "tokens_saved": parsed["total_saved_tokens"].as_i64().unwrap_or(0),
1783                    "tool_calls": parsed["total_calls"].as_i64().unwrap_or(0),
1784                    "cache_hits": parsed["cache_hits"].as_i64().unwrap_or(0),
1785                    "cache_misses": parsed["cache_misses"].as_i64().unwrap_or(0),
1786                });
1787                if crate::cloud_client::sync_stats(&[entry]).is_ok() {
1788                    config.cloud.last_sync = Some(today.clone());
1789                }
1790            }
1791        }
1792
1793        if !already_pulled {
1794            if let Ok(data) = crate::cloud_client::pull_pro_models() {
1795                let _ = crate::cloud_client::save_pro_models(&data);
1796                config.cloud.last_model_pull = Some(today.clone());
1797            }
1798        }
1799    }
1800
1801    let _ = config.save();
1802}
1803
1804pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
1805    build_instructions(crp_mode)
1806}
1807
1808pub fn tool_descriptions_for_test() -> Vec<(&'static str, &'static str)> {
1809    let mut result = Vec::new();
1810    let tools_json = list_all_tool_defs();
1811    for (name, desc, _) in tools_json {
1812        result.push((name, desc));
1813    }
1814    result
1815}
1816
1817pub fn tool_schemas_json_for_test() -> String {
1818    let tools_json = list_all_tool_defs();
1819    let schemas: Vec<String> = tools_json
1820        .iter()
1821        .map(|(name, _, schema)| format!("{}: {}", name, schema))
1822        .collect();
1823    schemas.join("\n")
1824}
1825
1826fn list_all_tool_defs() -> Vec<(&'static str, &'static str, Value)> {
1827    vec![
1828        ("ctx_read", "Read file (cached, compressed). Re-reads ~13 tok. Auto-selects optimal mode. \
1829Modes: full|map|signatures|diff|aggressive|entropy|task|reference|lines:N-M. fresh=true re-reads.", json!({"type": "object", "properties": {"path": {"type": "string"}, "mode": {"type": "string"}, "start_line": {"type": "integer"}, "fresh": {"type": "boolean"}}, "required": ["path"]})),
1830        ("ctx_multi_read", "Batch read files in one call. Same modes as ctx_read.", json!({"type": "object", "properties": {"paths": {"type": "array", "items": {"type": "string"}}, "mode": {"type": "string"}}, "required": ["paths"]})),
1831        ("ctx_tree", "Directory listing with file counts.", json!({"type": "object", "properties": {"path": {"type": "string"}, "depth": {"type": "integer"}, "show_hidden": {"type": "boolean"}}})),
1832        ("ctx_shell", "Run shell command (compressed output, 90+ patterns).", json!({"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]})),
1833        ("ctx_search", "Regex code search (.gitignore aware, compact results).", json!({"type": "object", "properties": {"pattern": {"type": "string"}, "path": {"type": "string"}, "ext": {"type": "string"}, "max_results": {"type": "integer"}}, "required": ["pattern"]})),
1834        ("ctx_compress", "Context checkpoint for long conversations.", json!({"type": "object", "properties": {"include_signatures": {"type": "boolean"}}})),
1835        ("ctx_benchmark", "Benchmark compression modes for a file or project.", json!({"type": "object", "properties": {"path": {"type": "string"}, "action": {"type": "string"}, "format": {"type": "string"}}, "required": ["path"]})),
1836        ("ctx_metrics", "Session token stats, cache rates, per-tool savings.", json!({"type": "object", "properties": {}})),
1837        ("ctx_analyze", "Entropy analysis — recommends optimal compression mode for a file.", json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]})),
1838        ("ctx_cache", "Cache ops: status|clear|invalidate.", json!({"type": "object", "properties": {"action": {"type": "string"}, "path": {"type": "string"}}, "required": ["action"]})),
1839        ("ctx_discover", "Find missed compression opportunities in shell history.", json!({"type": "object", "properties": {"limit": {"type": "integer"}}})),
1840        ("ctx_smart_read", "Auto-select optimal read mode for a file.", json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]})),
1841        ("ctx_delta", "Incremental diff — sends only changed lines since last read.", json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]})),
1842        ("ctx_edit", "Edit a file via search-and-replace. Works without native Read/Edit tools. Use when Edit requires Read but Read is unavailable.", json!({"type": "object", "properties": {"path": {"type": "string"}, "old_string": {"type": "string"}, "new_string": {"type": "string"}, "replace_all": {"type": "boolean"}, "create": {"type": "boolean"}}, "required": ["path", "new_string"]})),
1843        ("ctx_dedup", "Cross-file dedup: analyze or apply shared block references.", json!({"type": "object", "properties": {"action": {"type": "string"}}})),
1844        ("ctx_fill", "Budget-aware context fill — auto-selects compression per file within token limit.", json!({"type": "object", "properties": {"paths": {"type": "array", "items": {"type": "string"}}, "budget": {"type": "integer"}}, "required": ["paths", "budget"]})),
1845        ("ctx_intent", "Intent detection — auto-reads relevant files based on task description.", json!({"type": "object", "properties": {"query": {"type": "string"}, "project_root": {"type": "string"}}, "required": ["query"]})),
1846        ("ctx_response", "Compress LLM response text (remove filler, apply TDD).", json!({"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]})),
1847        ("ctx_context", "Session context overview — cached files, seen files, session state.", json!({"type": "object", "properties": {}})),
1848        ("ctx_graph", "Code dependency graph. Actions: build (index project), related (find files connected to path), \
1849symbol (lookup definition/usages as file::name), impact (blast radius of changes to path), status (index stats).", json!({"type": "object", "properties": {"action": {"type": "string"}, "path": {"type": "string"}, "project_root": {"type": "string"}}, "required": ["action"]})),
1850        ("ctx_session", "Cross-session memory (CCP). Actions: load (restore previous session ~400 tok), \
1851save, status, task (set current task), finding (record discovery), decision (record choice), \
1852reset, list (show sessions), cleanup.", json!({"type": "object", "properties": {"action": {"type": "string"}, "value": {"type": "string"}, "session_id": {"type": "string"}}, "required": ["action"]})),
1853        ("ctx_knowledge", "Persistent project knowledge (survives sessions). Actions: remember (store fact with category+key+value), \
1854recall (search by query), pattern (record naming/structure pattern), consolidate (extract session findings into knowledge), \
1855status (list all), remove, export.", json!({"type": "object", "properties": {"action": {"type": "string"}, "category": {"type": "string"}, "key": {"type": "string"}, "value": {"type": "string"}, "query": {"type": "string"}}, "required": ["action"]})),
1856        ("ctx_agent", "Multi-agent coordination (shared message bus). Actions: register (join with agent_type+role), \
1857post (broadcast or direct message with category), read (poll messages), status (update state: active|idle|finished), \
1858list, info.", json!({"type": "object", "properties": {"action": {"type": "string"}, "agent_type": {"type": "string"}, "role": {"type": "string"}, "message": {"type": "string"}}, "required": ["action"]})),
1859        ("ctx_overview", "Task-relevant project map — use at session start.", json!({"type": "object", "properties": {"task": {"type": "string"}, "path": {"type": "string"}}})),
1860        ("ctx_preload", "Proactive context loader — reads and caches task-relevant files, returns compact L-curve-optimized summary with critical lines, imports, and signatures. Costs ~50-100 tokens instead of ~5000 for individual reads.", json!({"type": "object", "properties": {"task": {"type": "string", "description": "Task description (e.g. 'fix auth bug in validate_token')"}, "path": {"type": "string", "description": "Project root (default: .)"}}, "required": ["task"]})),
1861        ("ctx_wrapped", "Savings report card. Periods: week|month|all.", json!({"type": "object", "properties": {"period": {"type": "string"}}})),
1862        ("ctx_semantic_search", "BM25 code search by meaning. action=reindex to rebuild.", json!({"type": "object", "properties": {"query": {"type": "string"}, "path": {"type": "string"}, "top_k": {"type": "integer"}, "action": {"type": "string"}}, "required": ["query"]})),
1863    ]
1864}
1865
1866#[cfg(test)]
1867mod tests {
1868    use super::*;
1869
1870    #[test]
1871    fn test_should_use_unified_defaults_to_false() {
1872        assert!(!should_use_unified("cursor"));
1873        assert!(!should_use_unified("claude-code"));
1874        assert!(!should_use_unified("windsurf"));
1875        assert!(!should_use_unified(""));
1876        assert!(!should_use_unified("some-unknown-client"));
1877    }
1878
1879    #[test]
1880    fn test_unified_tool_count() {
1881        let tools = unified_tool_defs();
1882        assert_eq!(tools.len(), 5, "Expected 5 unified tools");
1883    }
1884}