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