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.4"))
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.4"))
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                drop(cache);
632                {
633                    let mut session = self.session.write().await;
634                    session.touch_file(&path, file_ref.as_deref(), &effective_mode, original);
635                    if session.project_root.is_none() {
636                        if let Some(root) = detect_project_root(&path) {
637                            session.project_root = Some(root.clone());
638                            let mut current = self.agent_id.write().await;
639                            if current.is_none() {
640                                let mut registry =
641                                    crate::core::agents::AgentRegistry::load_or_create();
642                                registry.cleanup_stale(24);
643                                let id = registry.register("mcp", None, &root);
644                                let _ = registry.save();
645                                *current = Some(id);
646                            }
647                        }
648                    }
649                }
650                self.record_call("ctx_read", original, saved, Some(mode.clone()))
651                    .await;
652                {
653                    let sig =
654                        crate::core::mode_predictor::FileSignature::from_path(&path, original);
655                    let density = if output_tokens > 0 {
656                        original as f64 / output_tokens as f64
657                    } else {
658                        1.0
659                    };
660                    let outcome = crate::core::mode_predictor::ModeOutcome {
661                        mode: mode.clone(),
662                        tokens_in: original,
663                        tokens_out: output_tokens,
664                        density: density.min(1.0),
665                    };
666                    let mut predictor = crate::core::mode_predictor::ModePredictor::new();
667                    predictor.record(sig, outcome);
668                    predictor.save();
669
670                    let ext = std::path::Path::new(&path)
671                        .extension()
672                        .and_then(|e| e.to_str())
673                        .unwrap_or("")
674                        .to_string();
675                    let thresholds = crate::core::adaptive_thresholds::thresholds_for_path(&path);
676                    let cache = self.cache.read().await;
677                    let stats = cache.get_stats();
678                    let feedback_outcome = crate::core::feedback::CompressionOutcome {
679                        session_id: format!("{}", std::process::id()),
680                        language: ext,
681                        entropy_threshold: thresholds.bpe_entropy,
682                        jaccard_threshold: thresholds.jaccard,
683                        total_turns: stats.total_reads as u32,
684                        tokens_saved: saved as u64,
685                        tokens_original: original as u64,
686                        cache_hits: stats.cache_hits as u32,
687                        total_reads: stats.total_reads as u32,
688                        task_completed: true,
689                        timestamp: chrono::Local::now().to_rfc3339(),
690                    };
691                    drop(cache);
692                    let mut store = crate::core::feedback::FeedbackStore::load();
693                    store.record_outcome(feedback_outcome);
694                }
695                output
696            }
697            "ctx_multi_read" => {
698                let paths = get_str_array(args, "paths")
699                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
700                let mode = get_str(args, "mode").unwrap_or_else(|| "full".to_string());
701                let mut cache = self.cache.write().await;
702                let output =
703                    crate::tools::ctx_multi_read::handle(&mut cache, &paths, &mode, self.crp_mode);
704                let mut total_original: usize = 0;
705                for path in &paths {
706                    total_original = total_original
707                        .saturating_add(cache.get(path).map(|e| e.original_tokens).unwrap_or(0));
708                }
709                let tokens = crate::core::tokens::count_tokens(&output);
710                drop(cache);
711                self.record_call(
712                    "ctx_multi_read",
713                    total_original,
714                    total_original.saturating_sub(tokens),
715                    Some(mode),
716                )
717                .await;
718                output
719            }
720            "ctx_tree" => {
721                let path = get_str(args, "path").unwrap_or_else(|| ".".to_string());
722                let depth = get_int(args, "depth").unwrap_or(3) as usize;
723                let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
724                let (result, original) = crate::tools::ctx_tree::handle(&path, depth, show_hidden);
725                let sent = crate::core::tokens::count_tokens(&result);
726                let saved = original.saturating_sub(sent);
727                self.record_call("ctx_tree", original, saved, None).await;
728                let savings_note = if saved > 0 {
729                    format!("\n[saved {saved} tokens vs native ls]")
730                } else {
731                    String::new()
732                };
733                format!("{result}{savings_note}")
734            }
735            "ctx_shell" => {
736                let command = get_str(args, "command")
737                    .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
738                let output = execute_command(&command);
739                let result = crate::tools::ctx_shell::handle(&command, &output, self.crp_mode);
740                let original = crate::core::tokens::count_tokens(&output);
741                let sent = crate::core::tokens::count_tokens(&result);
742                let saved = original.saturating_sub(sent);
743                self.record_call("ctx_shell", original, saved, None).await;
744                let savings_note = if saved > 0 {
745                    format!("\n[saved {saved} tokens vs native Shell]")
746                } else {
747                    String::new()
748                };
749                format!("{result}{savings_note}")
750            }
751            "ctx_search" => {
752                let pattern = get_str(args, "pattern")
753                    .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
754                let path = get_str(args, "path").unwrap_or_else(|| ".".to_string());
755                let ext = get_str(args, "ext");
756                let max = get_int(args, "max_results").unwrap_or(20) as usize;
757                let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
758                let (result, original) = crate::tools::ctx_search::handle(
759                    &pattern,
760                    &path,
761                    ext.as_deref(),
762                    max,
763                    self.crp_mode,
764                    !no_gitignore,
765                );
766                let sent = crate::core::tokens::count_tokens(&result);
767                let saved = original.saturating_sub(sent);
768                self.record_call("ctx_search", original, saved, None).await;
769                let savings_note = if saved > 0 {
770                    format!("\n[saved {saved} tokens vs native Grep]")
771                } else {
772                    String::new()
773                };
774                format!("{result}{savings_note}")
775            }
776            "ctx_compress" => {
777                let include_sigs = get_bool(args, "include_signatures").unwrap_or(true);
778                let cache = self.cache.read().await;
779                let result =
780                    crate::tools::ctx_compress::handle(&cache, include_sigs, self.crp_mode);
781                drop(cache);
782                self.record_call("ctx_compress", 0, 0, None).await;
783                result
784            }
785            "ctx_benchmark" => {
786                let path = get_str(args, "path")
787                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
788                let action = get_str(args, "action").unwrap_or_default();
789                let result = if action == "project" {
790                    let fmt = get_str(args, "format").unwrap_or_default();
791                    let bench = crate::core::benchmark::run_project_benchmark(&path);
792                    match fmt.as_str() {
793                        "json" => crate::core::benchmark::format_json(&bench),
794                        "markdown" | "md" => crate::core::benchmark::format_markdown(&bench),
795                        _ => crate::core::benchmark::format_terminal(&bench),
796                    }
797                } else {
798                    crate::tools::ctx_benchmark::handle(&path, self.crp_mode)
799                };
800                self.record_call("ctx_benchmark", 0, 0, None).await;
801                result
802            }
803            "ctx_metrics" => {
804                let cache = self.cache.read().await;
805                let calls = self.tool_calls.read().await;
806                let result = crate::tools::ctx_metrics::handle(&cache, &calls, self.crp_mode);
807                drop(cache);
808                drop(calls);
809                self.record_call("ctx_metrics", 0, 0, None).await;
810                result
811            }
812            "ctx_analyze" => {
813                let path = get_str(args, "path")
814                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
815                let result = crate::tools::ctx_analyze::handle(&path, self.crp_mode);
816                self.record_call("ctx_analyze", 0, 0, None).await;
817                result
818            }
819            "ctx_discover" => {
820                let limit = get_int(args, "limit").unwrap_or(15) as usize;
821                let history = crate::cli::load_shell_history_pub();
822                let result = crate::tools::ctx_discover::discover_from_history(&history, limit);
823                self.record_call("ctx_discover", 0, 0, None).await;
824                result
825            }
826            "ctx_smart_read" => {
827                let path = get_str(args, "path")
828                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
829                let mut cache = self.cache.write().await;
830                let output = crate::tools::ctx_smart_read::handle(&mut cache, &path, self.crp_mode);
831                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
832                let tokens = crate::core::tokens::count_tokens(&output);
833                drop(cache);
834                self.record_call(
835                    "ctx_smart_read",
836                    original,
837                    original.saturating_sub(tokens),
838                    Some("auto".to_string()),
839                )
840                .await;
841                output
842            }
843            "ctx_delta" => {
844                let path = get_str(args, "path")
845                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
846                let mut cache = self.cache.write().await;
847                let output = crate::tools::ctx_delta::handle(&mut cache, &path);
848                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
849                let tokens = crate::core::tokens::count_tokens(&output);
850                drop(cache);
851                {
852                    let mut session = self.session.write().await;
853                    session.mark_modified(&path);
854                }
855                self.record_call(
856                    "ctx_delta",
857                    original,
858                    original.saturating_sub(tokens),
859                    Some("delta".to_string()),
860                )
861                .await;
862                output
863            }
864            "ctx_dedup" => {
865                let action = get_str(args, "action").unwrap_or_default();
866                if action == "apply" {
867                    let mut cache = self.cache.write().await;
868                    let result = crate::tools::ctx_dedup::handle_action(&mut cache, &action);
869                    drop(cache);
870                    self.record_call("ctx_dedup", 0, 0, None).await;
871                    result
872                } else {
873                    let cache = self.cache.read().await;
874                    let result = crate::tools::ctx_dedup::handle(&cache);
875                    drop(cache);
876                    self.record_call("ctx_dedup", 0, 0, None).await;
877                    result
878                }
879            }
880            "ctx_fill" => {
881                let paths = get_str_array(args, "paths")
882                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
883                let budget = get_int(args, "budget")
884                    .ok_or_else(|| ErrorData::invalid_params("budget is required", None))?
885                    as usize;
886                let mut cache = self.cache.write().await;
887                let output =
888                    crate::tools::ctx_fill::handle(&mut cache, &paths, budget, self.crp_mode);
889                drop(cache);
890                self.record_call("ctx_fill", 0, 0, Some(format!("budget:{budget}")))
891                    .await;
892                output
893            }
894            "ctx_intent" => {
895                let query = get_str(args, "query")
896                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
897                let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
898                let mut cache = self.cache.write().await;
899                let output =
900                    crate::tools::ctx_intent::handle(&mut cache, &query, &root, self.crp_mode);
901                drop(cache);
902                {
903                    let mut session = self.session.write().await;
904                    session.set_task(&query, Some("intent"));
905                }
906                self.record_call("ctx_intent", 0, 0, Some("semantic".to_string()))
907                    .await;
908                output
909            }
910            "ctx_response" => {
911                let text = get_str(args, "text")
912                    .ok_or_else(|| ErrorData::invalid_params("text is required", None))?;
913                let output = crate::tools::ctx_response::handle(&text, self.crp_mode);
914                self.record_call("ctx_response", 0, 0, None).await;
915                output
916            }
917            "ctx_context" => {
918                let cache = self.cache.read().await;
919                let turn = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
920                let result = crate::tools::ctx_context::handle_status(&cache, turn, self.crp_mode);
921                drop(cache);
922                self.record_call("ctx_context", 0, 0, None).await;
923                result
924            }
925            "ctx_graph" => {
926                let action = get_str(args, "action")
927                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
928                let path = get_str(args, "path");
929                let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
930                let mut cache = self.cache.write().await;
931                let result = crate::tools::ctx_graph::handle(
932                    &action,
933                    path.as_deref(),
934                    &root,
935                    &mut cache,
936                    self.crp_mode,
937                );
938                drop(cache);
939                self.record_call("ctx_graph", 0, 0, Some(action)).await;
940                result
941            }
942            "ctx_cache" => {
943                let action = get_str(args, "action")
944                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
945                let mut cache = self.cache.write().await;
946                let result = match action.as_str() {
947                    "status" => {
948                        let entries = cache.get_all_entries();
949                        if entries.is_empty() {
950                            "Cache empty — no files tracked.".to_string()
951                        } else {
952                            let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
953                            for (path, entry) in &entries {
954                                let fref = cache
955                                    .file_ref_map()
956                                    .get(*path)
957                                    .map(|s| s.as_str())
958                                    .unwrap_or("F?");
959                                lines.push(format!(
960                                    "  {fref}={} [{}L, {}t, read {}x]",
961                                    crate::core::protocol::shorten_path(path),
962                                    entry.line_count,
963                                    entry.original_tokens,
964                                    entry.read_count
965                                ));
966                            }
967                            lines.join("\n")
968                        }
969                    }
970                    "clear" => {
971                        let count = cache.clear();
972                        format!("Cache cleared — {count} file(s) removed. Next ctx_read will return full content.")
973                    }
974                    "invalidate" => {
975                        let path = get_str(args, "path").ok_or_else(|| {
976                            ErrorData::invalid_params("path is required for invalidate", None)
977                        })?;
978                        if cache.invalidate(&path) {
979                            format!(
980                                "Invalidated cache for {}. Next ctx_read will return full content.",
981                                crate::core::protocol::shorten_path(&path)
982                            )
983                        } else {
984                            format!(
985                                "{} was not in cache.",
986                                crate::core::protocol::shorten_path(&path)
987                            )
988                        }
989                    }
990                    _ => "Unknown action. Use: status, clear, invalidate".to_string(),
991                };
992                drop(cache);
993                self.record_call("ctx_cache", 0, 0, Some(action)).await;
994                result
995            }
996            "ctx_session" => {
997                let action = get_str(args, "action")
998                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
999                let value = get_str(args, "value");
1000                let sid = get_str(args, "session_id");
1001                let mut session = self.session.write().await;
1002                let result = crate::tools::ctx_session::handle(
1003                    &mut session,
1004                    &action,
1005                    value.as_deref(),
1006                    sid.as_deref(),
1007                );
1008                drop(session);
1009                self.record_call("ctx_session", 0, 0, Some(action)).await;
1010                result
1011            }
1012            "ctx_knowledge" => {
1013                let action = get_str(args, "action")
1014                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1015                let category = get_str(args, "category");
1016                let key = get_str(args, "key");
1017                let value = get_str(args, "value");
1018                let query = get_str(args, "query");
1019                let pattern_type = get_str(args, "pattern_type");
1020                let examples = get_str_array(args, "examples");
1021                let confidence: Option<f32> = args
1022                    .as_ref()
1023                    .and_then(|a| a.get("confidence"))
1024                    .and_then(|v| v.as_f64())
1025                    .map(|v| v as f32);
1026
1027                let session = self.session.read().await;
1028                let session_id = session.id.clone();
1029                let project_root = session.project_root.clone().unwrap_or_else(|| {
1030                    std::env::current_dir()
1031                        .map(|p| p.to_string_lossy().to_string())
1032                        .unwrap_or_else(|_| "unknown".to_string())
1033                });
1034                drop(session);
1035
1036                let result = crate::tools::ctx_knowledge::handle(
1037                    &project_root,
1038                    &action,
1039                    category.as_deref(),
1040                    key.as_deref(),
1041                    value.as_deref(),
1042                    query.as_deref(),
1043                    &session_id,
1044                    pattern_type.as_deref(),
1045                    examples,
1046                    confidence,
1047                );
1048                self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
1049                result
1050            }
1051            "ctx_agent" => {
1052                let action = get_str(args, "action")
1053                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1054                let agent_type = get_str(args, "agent_type");
1055                let role = get_str(args, "role");
1056                let message = get_str(args, "message");
1057                let category = get_str(args, "category");
1058                let to_agent = get_str(args, "to_agent");
1059                let status = get_str(args, "status");
1060
1061                let session = self.session.read().await;
1062                let project_root = session.project_root.clone().unwrap_or_else(|| {
1063                    std::env::current_dir()
1064                        .map(|p| p.to_string_lossy().to_string())
1065                        .unwrap_or_else(|_| "unknown".to_string())
1066                });
1067                drop(session);
1068
1069                let current_agent_id = self.agent_id.read().await.clone();
1070                let result = crate::tools::ctx_agent::handle(
1071                    &action,
1072                    agent_type.as_deref(),
1073                    role.as_deref(),
1074                    &project_root,
1075                    current_agent_id.as_deref(),
1076                    message.as_deref(),
1077                    category.as_deref(),
1078                    to_agent.as_deref(),
1079                    status.as_deref(),
1080                );
1081
1082                if action == "register" {
1083                    if let Some(id) = result.split(':').nth(1) {
1084                        let id = id.split_whitespace().next().unwrap_or("").to_string();
1085                        if !id.is_empty() {
1086                            *self.agent_id.write().await = Some(id);
1087                        }
1088                    }
1089                }
1090
1091                self.record_call("ctx_agent", 0, 0, Some(action)).await;
1092                result
1093            }
1094            "ctx_overview" => {
1095                let task = get_str(args, "task");
1096                let path = get_str(args, "path");
1097                let cache = self.cache.read().await;
1098                let result = crate::tools::ctx_overview::handle(
1099                    &cache,
1100                    task.as_deref(),
1101                    path.as_deref(),
1102                    self.crp_mode,
1103                );
1104                drop(cache);
1105                self.record_call("ctx_overview", 0, 0, Some("overview".to_string()))
1106                    .await;
1107                result
1108            }
1109            "ctx_wrapped" => {
1110                let period = get_str(args, "period").unwrap_or_else(|| "week".to_string());
1111                let result = crate::tools::ctx_wrapped::handle(&period);
1112                self.record_call("ctx_wrapped", 0, 0, Some(period)).await;
1113                result
1114            }
1115            "ctx_semantic_search" => {
1116                let query = get_str(args, "query")
1117                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
1118                let path = get_str(args, "path").unwrap_or_else(|| ".".to_string());
1119                let top_k = get_int(args, "top_k").unwrap_or(10) as usize;
1120                let action = get_str(args, "action").unwrap_or_default();
1121                let result = if action == "reindex" {
1122                    crate::tools::ctx_semantic_search::handle_reindex(&path)
1123                } else {
1124                    crate::tools::ctx_semantic_search::handle(&query, &path, top_k, self.crp_mode)
1125                };
1126                self.record_call("ctx_semantic_search", 0, 0, Some("semantic".to_string()))
1127                    .await;
1128                result
1129            }
1130            _ => {
1131                return Err(ErrorData::invalid_params(
1132                    format!("Unknown tool: {name}"),
1133                    None,
1134                ));
1135            }
1136        };
1137
1138        let skip_checkpoint = matches!(
1139            name,
1140            "ctx_compress"
1141                | "ctx_metrics"
1142                | "ctx_benchmark"
1143                | "ctx_analyze"
1144                | "ctx_cache"
1145                | "ctx_discover"
1146                | "ctx_dedup"
1147                | "ctx_session"
1148                | "ctx_knowledge"
1149                | "ctx_agent"
1150                | "ctx_wrapped"
1151                | "ctx_overview"
1152        );
1153
1154        if !skip_checkpoint && self.increment_and_check() {
1155            if let Some(checkpoint) = self.auto_checkpoint().await {
1156                let combined = format!(
1157                    "{result_text}\n\n--- AUTO CHECKPOINT (every {} calls) ---\n{checkpoint}",
1158                    self.checkpoint_interval
1159                );
1160                return Ok(CallToolResult::success(vec![Content::text(combined)]));
1161            }
1162        }
1163
1164        let current_count = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
1165        if current_count > 0 && current_count.is_multiple_of(100) {
1166            std::thread::spawn(cloud_background_tasks);
1167        }
1168
1169        Ok(CallToolResult::success(vec![Content::text(result_text)]))
1170    }
1171}
1172
1173fn build_instructions(crp_mode: CrpMode) -> String {
1174    build_instructions_with_client(crp_mode, "")
1175}
1176
1177fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
1178    let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
1179    let session_block = match crate::core::session::SessionState::load_latest() {
1180        Some(ref session) => {
1181            let positioned = crate::core::litm::position_optimize(session);
1182            format!(
1183                "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
1184                profile.name, positioned.begin_block
1185            )
1186        }
1187        None => String::new(),
1188    };
1189
1190    let knowledge_block = {
1191        let project_root = crate::core::session::SessionState::load_latest()
1192            .and_then(|s| s.project_root)
1193            .or_else(|| {
1194                std::env::current_dir()
1195                    .ok()
1196                    .map(|p| p.to_string_lossy().to_string())
1197            });
1198        match project_root {
1199            Some(root) => {
1200                let knowledge = crate::core::knowledge::ProjectKnowledge::load(&root);
1201                match knowledge {
1202                    Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
1203                        format!("\n--- PROJECT KNOWLEDGE ---\n{}\n---\n", k.format_summary())
1204                    }
1205                    _ => String::new(),
1206                }
1207            }
1208            None => String::new(),
1209        }
1210    };
1211
1212    // Prefix-cache alignment: stable instructions first (API providers cache KV states
1213    // for shared prefixes), then variable session state after.
1214    let mut base = format!("\
1215CRITICAL: 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\
1216\n\
1217lean-ctx MCP — tool replacement for reading, running commands, and searching.\n\
1218\n\
1219REPLACE these built-in tools with lean-ctx equivalents:\n\
1220• Read file → ctx_read(path, mode) — NEVER use Read tool\n\
1221• Run command → ctx_shell(command) — NEVER use Shell tool\n\
1222• Search code → ctx_search(pattern, path) — NEVER use Grep tool\n\
1223• List files → ctx_tree(path, depth) — NEVER use Shell with ls/find\n\
1224\n\
1225KEEP using these built-in tools normally (lean-ctx has NO replacement for them):\n\
1226• Write — create/overwrite files directly\n\
1227• StrReplace — edit files directly\n\
1228• Delete — delete files directly\n\
1229• Glob — find files by pattern\n\
1230You do NOT need to ctx_read a file before creating it with Write.\n\
1231\n\
1232ctx_read modes: full (cached, for files you edit), map (deps+API, context-only), \
1233signatures, diff, task (IB-filtered task-relevant lines), reference (one-line metadata), \
1234aggressive, entropy, lines:N-M (specific line ranges). \
1235Auto-selects optimal mode when none specified. Re-reads cost ~13 tokens. File refs F1,F2.. persist.\n\
1236IMPORTANT: If ctx_read returns 'cached Nt NL' and you need the actual file content, you MUST either:\n\
1237  1. Set fresh=true to force a full re-read, OR\n\
1238  2. Use start_line=N to read from a specific line, OR\n\
1239  3. Use mode='lines:N-M' to read a specific range.\n\
1240Do not fall back to native Read tools — always use fresh=true or start_line instead.\n\
1241\n\
1242PROACTIVE (use without being asked):\n\
1243• ctx_overview(task) — at session start, get task-relevant project map\n\
1244• ctx_compress — when context grows large, create checkpoint\n\
1245• ctx_session load — restore previous session on new chat\n\
1246\n\
1247ADDITIONAL TOOLS (see tool descriptions for parameters):\n\
1248• ctx_session — cross-session memory (load/save/status/task/finding/decision)\n\
1249• ctx_knowledge — persistent project facts (remember/recall/pattern/status/remove/consolidate)\n\
1250• ctx_agent — multi-agent coordination (register/list/post/read/status)\n\
1251• ctx_metrics — token savings stats\n\
1252• ctx_analyze/ctx_benchmark — compression analysis per file\n\
1253• ctx_cache — manage file cache (status/clear/invalidate)\n\
1254• ctx_wrapped — savings report card\n\
1255• ctx_compress — context checkpoint\n\
1256\n\
1257Auto-checkpoint runs every 15 tool calls. Cache auto-clears after 5 min idle.\n\
1258\n\
1259COMMUNICATION PROTOCOL (CEP v1):\n\
12601. ACT FIRST — Execute tool calls immediately, summarize after.\n\
12612. DELTA ONLY — Reference cached files by Fn ID, never repeat known context.\n\
12623. STRUCTURED OVER PROSE — Use notation: +line / -line / ~line, tool(args) → result.\n\
12634. ONE LINE PER ACTION — Summarize, don't explain.\n\
12645. QUALITY ANCHOR — Never skip edge case analysis to save tokens.\n\
1265\n\
1266{decoder_block}\n\
1267\n\
1268REMINDER: NEVER use native Read, Shell, Grep, or ls. ALWAYS use ctx_read, ctx_shell, ctx_search, ctx_tree. Every single time.\n\
1269\n\
1270{session_block}\
1271{knowledge_block}",
1272        decoder_block = crate::core::protocol::instruction_decoder_block()
1273    );
1274
1275    if should_use_unified(client_name) {
1276        base.push_str(
1277            "\n\n\
1278UNIFIED TOOL MODE (active):\n\
1279Additional tools are accessed via ctx() meta-tool: ctx(tool=\"<name>\", ...params).\n\
1280See the ctx() tool description for available sub-tools.\n",
1281        );
1282    }
1283
1284    let base = base;
1285    match crp_mode {
1286        CrpMode::Off => base,
1287        CrpMode::Compact => {
1288            format!(
1289                "{base}\n\n\
1290                CRP MODE: compact\n\
1291                Respond using Compact Response Protocol:\n\
1292                • Omit filler words, articles, and redundant phrases\n\
1293                • Use symbol shorthand: → ∴ ≈ ✓ ✗\n\
1294                • Abbreviate: fn, cfg, impl, deps, req, res, ctx, err, ok, ret, arg, val, ty, mod\n\
1295                • Use compact lists instead of prose\n\
1296                • Prefer code blocks over natural language explanations\n\
1297                • For code changes: show only diff lines (+/-), not full files"
1298            )
1299        }
1300        CrpMode::Tdd => {
1301            format!(
1302                "{base}\n\n\
1303                CRP MODE: tdd (Token Dense Dialect)\n\
1304                CRITICAL: Maximize information density. Every token must carry meaning.\n\
1305                \n\
1306                RESPONSE RULES:\n\
1307                • Drop all articles (a, the, an), filler words, and pleasantries\n\
1308                • Reference files by Fn refs only, never full paths\n\
1309                • For code changes: show only diff lines, not full files\n\
1310                • No explanations unless asked — just show the solution\n\
1311                • Use tabular format for structured data\n\
1312                • Abbreviations: fn, cfg, impl, deps, req, res, ctx, err, ok, ret, arg, val, ty, mod\n\
1313                \n\
1314                SYMBOLS (each = 1 token, replaces 5-10 tokens of prose):\n\
1315                Structural: λ=function  §=module/struct  ∂=interface/trait  τ=type  ε=enum\n\
1316                Actions:    ⊕=add  ⊖=remove  ∆=modify  →=returns  ⇒=implies\n\
1317                Status:     ✓=ok  ✗=fail  ⚠=warning\n\
1318                \n\
1319                CHANGE NOTATION (use for all code modifications):\n\
1320                ⊕F1:42 param(timeout:Duration)     — added parameter\n\
1321                ⊖F1:10-15                           — removed lines\n\
1322                ∆F1:42 validate_token → verify_jwt  — renamed/refactored\n\
1323                \n\
1324                STATUS NOTATION:\n\
1325                ctx_read(F1) → 808L cached ✓\n\
1326                cargo test → 82 passed ✓ 0 failed\n\
1327                \n\
1328                SYMBOL TABLE: Tool outputs include a §MAP section mapping long identifiers to short IDs.\n\
1329                Use these short IDs in all subsequent references."
1330            )
1331        }
1332    }
1333}
1334
1335fn tool_def(name: &'static str, description: &'static str, schema_value: Value) -> Tool {
1336    let schema: Map<String, Value> = match schema_value {
1337        Value::Object(map) => map,
1338        _ => Map::new(),
1339    };
1340    Tool::new(name, description, Arc::new(schema))
1341}
1342
1343fn unified_tool_defs() -> Vec<Tool> {
1344    vec![
1345        tool_def(
1346            "ctx_read",
1347            "Read files with caching + 8 compression modes. REPLACES native Read. \
1348            Auto-selects optimal mode when none specified. Re-reads ~13 tok. \
1349            Modes: full, map, signatures, diff, aggressive, entropy, task, reference, lines:N-M. \
1350            fresh=true bypasses cache.",
1351            json!({
1352                "type": "object",
1353                "properties": {
1354                    "path": { "type": "string", "description": "File path" },
1355                    "mode": { "type": "string" },
1356                    "start_line": { "type": "integer" },
1357                    "fresh": { "type": "boolean" }
1358                },
1359                "required": ["path"]
1360            }),
1361        ),
1362        tool_def(
1363            "ctx_shell",
1364            "Run shell commands with output compression. REPLACES native Shell.",
1365            json!({
1366                "type": "object",
1367                "properties": {
1368                    "command": { "type": "string", "description": "Shell command" }
1369                },
1370                "required": ["command"]
1371            }),
1372        ),
1373        tool_def(
1374            "ctx_search",
1375            "Search code with regex patterns. REPLACES native Grep. Respects .gitignore.",
1376            json!({
1377                "type": "object",
1378                "properties": {
1379                    "pattern": { "type": "string", "description": "Regex pattern" },
1380                    "path": { "type": "string" },
1381                    "ext": { "type": "string" },
1382                    "max_results": { "type": "integer" },
1383                    "ignore_gitignore": { "type": "boolean" }
1384                },
1385                "required": ["pattern"]
1386            }),
1387        ),
1388        tool_def(
1389            "ctx_tree",
1390            "List directory contents with file counts. REPLACES native ls/find.",
1391            json!({
1392                "type": "object",
1393                "properties": {
1394                    "path": { "type": "string" },
1395                    "depth": { "type": "integer" },
1396                    "show_hidden": { "type": "boolean" }
1397                }
1398            }),
1399        ),
1400        tool_def(
1401            "ctx",
1402            "Lean-ctx meta-tool — 21 sub-tools via single endpoint. Set 'tool' param + sibling fields.\n\
1403            Sub-tools:\n\
1404            • compress — create context checkpoint\n\
1405            • metrics — show token savings stats\n\
1406            • analyze(path) — optimal compression mode for file\n\
1407            • cache(action=status|clear|invalidate) — manage file cache\n\
1408            • discover — find missed compression opportunities\n\
1409            • smart_read(path) — auto-select best read mode\n\
1410            • delta(path) — show only changed lines since last read\n\
1411            • dedup(paths) — deduplicate across files\n\
1412            • fill(path) — suggest next likely edit location\n\
1413            • intent(text) — classify user intent for routing\n\
1414            • response(text) — compress LLM output, remove filler\n\
1415            • context(budget) — budget-aware context assembly\n\
1416            • graph(action=build|query|impact) — code dependency graph\n\
1417            • session(action=load|save|status|task|finding|decision) — cross-session memory\n\
1418            • knowledge(action=remember|recall|pattern|status|remove|consolidate) — persistent project memory\n\
1419            • agent(action=register|list|post|read|status) — multi-agent coordination\n\
1420            • overview(task) — task-relevant project map\n\
1421            • wrapped(period) — savings report card\n\
1422            • benchmark(path) — token counts per compression mode\n\
1423            • multi_read(paths) — batch file read\n\
1424            • semantic_search(query, path?, limit?) — BM25 code search",
1425            json!({
1426                "type": "object",
1427                "properties": {
1428                    "tool": {
1429                        "type": "string",
1430                        "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"
1431                    },
1432                    "action": { "type": "string" },
1433                    "path": { "type": "string" },
1434                    "paths": { "type": "array", "items": { "type": "string" } },
1435                    "query": { "type": "string" },
1436                    "value": { "type": "string" },
1437                    "category": { "type": "string" },
1438                    "key": { "type": "string" },
1439                    "budget": { "type": "integer" },
1440                    "task": { "type": "string" },
1441                    "mode": { "type": "string" },
1442                    "text": { "type": "string" },
1443                    "message": { "type": "string" },
1444                    "session_id": { "type": "string" },
1445                    "period": { "type": "string" },
1446                    "format": { "type": "string" },
1447                    "agent_type": { "type": "string" },
1448                    "role": { "type": "string" },
1449                    "status": { "type": "string" },
1450                    "pattern_type": { "type": "string" },
1451                    "examples": { "type": "array", "items": { "type": "string" } },
1452                    "confidence": { "type": "number" },
1453                    "project_root": { "type": "string" },
1454                    "include_signatures": { "type": "boolean" },
1455                    "limit": { "type": "integer" },
1456                    "to_agent": { "type": "string" },
1457                    "show_hidden": { "type": "boolean" }
1458                },
1459                "required": ["tool"]
1460            }),
1461        ),
1462    ]
1463}
1464
1465fn should_use_unified(client_name: &str) -> bool {
1466    if std::env::var("LEAN_CTX_FULL_TOOLS").is_ok() {
1467        return false;
1468    }
1469    if std::env::var("LEAN_CTX_UNIFIED").is_ok() {
1470        return true;
1471    }
1472    let _ = client_name;
1473    false
1474}
1475
1476fn get_str_array(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<Vec<String>> {
1477    let arr = args.as_ref()?.get(key)?.as_array()?;
1478    let mut out = Vec::with_capacity(arr.len());
1479    for v in arr {
1480        let s = v.as_str()?.to_string();
1481        out.push(s);
1482    }
1483    Some(out)
1484}
1485
1486fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
1487    args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
1488}
1489
1490fn get_int(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
1491    args.as_ref()?.get(key)?.as_i64()
1492}
1493
1494fn get_bool(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
1495    args.as_ref()?.get(key)?.as_bool()
1496}
1497
1498fn execute_command(command: &str) -> String {
1499    let (shell, flag) = crate::shell::shell_and_flag();
1500    let output = std::process::Command::new(&shell)
1501        .arg(&flag)
1502        .arg(command)
1503        .env("LEAN_CTX_ACTIVE", "1")
1504        .output();
1505
1506    match output {
1507        Ok(out) => {
1508            let stdout = String::from_utf8_lossy(&out.stdout);
1509            let stderr = String::from_utf8_lossy(&out.stderr);
1510            if stdout.is_empty() {
1511                stderr.to_string()
1512            } else if stderr.is_empty() {
1513                stdout.to_string()
1514            } else {
1515                format!("{stdout}\n{stderr}")
1516            }
1517        }
1518        Err(e) => format!("ERROR: {e}"),
1519    }
1520}
1521
1522fn detect_project_root(file_path: &str) -> Option<String> {
1523    let mut dir = std::path::Path::new(file_path).parent()?;
1524    loop {
1525        if dir.join(".git").exists() {
1526            return Some(dir.to_string_lossy().to_string());
1527        }
1528        dir = dir.parent()?;
1529    }
1530}
1531
1532fn cloud_background_tasks() {
1533    use crate::core::config::Config;
1534
1535    let mut config = Config::load();
1536    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
1537
1538    let already_contributed = config
1539        .cloud
1540        .last_contribute
1541        .as_deref()
1542        .map(|d| d == today)
1543        .unwrap_or(false);
1544    let already_synced = config
1545        .cloud
1546        .last_sync
1547        .as_deref()
1548        .map(|d| d == today)
1549        .unwrap_or(false);
1550    let already_pulled = config
1551        .cloud
1552        .last_model_pull
1553        .as_deref()
1554        .map(|d| d == today)
1555        .unwrap_or(false);
1556
1557    if config.cloud.contribute_enabled && !already_contributed {
1558        if let Some(home) = dirs::home_dir() {
1559            let mode_stats_path = home.join(".lean-ctx").join("mode_stats.json");
1560            if let Ok(data) = std::fs::read_to_string(&mode_stats_path) {
1561                if let Ok(predictor) = serde_json::from_str::<serde_json::Value>(&data) {
1562                    let mut entries = Vec::new();
1563                    if let Some(history) = predictor["history"].as_object() {
1564                        for (_key, outcomes) in history {
1565                            if let Some(arr) = outcomes.as_array() {
1566                                for outcome in arr.iter().rev().take(3) {
1567                                    let ext = outcome["ext"].as_str().unwrap_or("unknown");
1568                                    let mode = outcome["mode"].as_str().unwrap_or("full");
1569                                    let t_in = outcome["tokens_in"].as_u64().unwrap_or(0);
1570                                    let t_out = outcome["tokens_out"].as_u64().unwrap_or(0);
1571                                    let ratio = if t_in > 0 {
1572                                        1.0 - t_out as f64 / t_in as f64
1573                                    } else {
1574                                        0.0
1575                                    };
1576                                    let bucket = match t_in {
1577                                        0..=500 => "0-500",
1578                                        501..=2000 => "500-2k",
1579                                        2001..=10000 => "2k-10k",
1580                                        _ => "10k+",
1581                                    };
1582                                    entries.push(serde_json::json!({
1583                                        "file_ext": format!(".{ext}"),
1584                                        "size_bucket": bucket,
1585                                        "best_mode": mode,
1586                                        "compression_ratio": (ratio * 100.0).round() / 100.0,
1587                                    }));
1588                                    if entries.len() >= 200 {
1589                                        break;
1590                                    }
1591                                }
1592                            }
1593                            if entries.len() >= 200 {
1594                                break;
1595                            }
1596                        }
1597                    }
1598                    if !entries.is_empty() && crate::cloud_client::contribute(&entries).is_ok() {
1599                        config.cloud.last_contribute = Some(today.clone());
1600                    }
1601                }
1602            }
1603        }
1604    }
1605
1606    if crate::cloud_client::check_pro() {
1607        if !already_synced {
1608            let stats_data = crate::core::stats::format_gain_json();
1609            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&stats_data) {
1610                let entry = serde_json::json!({
1611                    "date": &today,
1612                    "tokens_original": parsed["total_original_tokens"].as_i64().unwrap_or(0),
1613                    "tokens_compressed": parsed["total_compressed_tokens"].as_i64().unwrap_or(0),
1614                    "tokens_saved": parsed["total_saved_tokens"].as_i64().unwrap_or(0),
1615                    "tool_calls": parsed["total_calls"].as_i64().unwrap_or(0),
1616                    "cache_hits": parsed["cache_hits"].as_i64().unwrap_or(0),
1617                    "cache_misses": parsed["cache_misses"].as_i64().unwrap_or(0),
1618                });
1619                if crate::cloud_client::sync_stats(&[entry]).is_ok() {
1620                    config.cloud.last_sync = Some(today.clone());
1621                }
1622            }
1623        }
1624
1625        if !already_pulled {
1626            if let Ok(data) = crate::cloud_client::pull_pro_models() {
1627                let _ = crate::cloud_client::save_pro_models(&data);
1628                config.cloud.last_model_pull = Some(today.clone());
1629            }
1630        }
1631    }
1632
1633    let _ = config.save();
1634}
1635
1636#[cfg(test)]
1637mod tests {
1638    use super::*;
1639
1640    #[test]
1641    fn test_should_use_unified_defaults_to_false() {
1642        assert!(!should_use_unified("cursor"));
1643        assert!(!should_use_unified("claude-code"));
1644        assert!(!should_use_unified("windsurf"));
1645        assert!(!should_use_unified(""));
1646        assert!(!should_use_unified("some-unknown-client"));
1647    }
1648
1649    #[test]
1650    fn test_unified_tool_count() {
1651        let tools = unified_tool_defs();
1652        assert_eq!(tools.len(), 5, "Expected 5 unified tools");
1653    }
1654}