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