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