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