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