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