claude-mpm 4.3.6__py3-none-any.whl → 4.3.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_PM.md +41 -8
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +85 -43
  4. claude_mpm/agents/templates/clerk-ops.json +223 -0
  5. claude_mpm/agents/templates/data_engineer.json +41 -5
  6. claude_mpm/agents/templates/php-engineer.json +185 -0
  7. claude_mpm/agents/templates/research.json +20 -8
  8. claude_mpm/agents/templates/web_qa.json +25 -10
  9. claude_mpm/cli/__init__.py +41 -2
  10. claude_mpm/cli/commands/agents.py +2 -2
  11. claude_mpm/cli/commands/analyze.py +4 -4
  12. claude_mpm/cli/commands/cleanup.py +7 -7
  13. claude_mpm/cli/commands/configure_tui.py +2 -2
  14. claude_mpm/cli/commands/debug.py +2 -2
  15. claude_mpm/cli/commands/info.py +3 -4
  16. claude_mpm/cli/commands/mcp.py +8 -6
  17. claude_mpm/cli/commands/mcp_command_router.py +11 -0
  18. claude_mpm/cli/commands/mcp_config.py +157 -0
  19. claude_mpm/cli/commands/mcp_external_commands.py +241 -0
  20. claude_mpm/cli/commands/mcp_install_commands.py +73 -32
  21. claude_mpm/cli/commands/mcp_setup_external.py +829 -0
  22. claude_mpm/cli/commands/run.py +73 -3
  23. claude_mpm/cli/commands/search.py +285 -0
  24. claude_mpm/cli/parsers/base_parser.py +13 -0
  25. claude_mpm/cli/parsers/mcp_parser.py +17 -0
  26. claude_mpm/cli/parsers/run_parser.py +5 -0
  27. claude_mpm/cli/parsers/search_parser.py +239 -0
  28. claude_mpm/cli/startup_logging.py +20 -7
  29. claude_mpm/constants.py +1 -0
  30. claude_mpm/core/unified_agent_registry.py +7 -0
  31. claude_mpm/hooks/instruction_reinforcement.py +295 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +28 -13
  33. claude_mpm/services/agents/deployment/agent_discovery_service.py +16 -6
  34. claude_mpm/services/agents/deployment/deployment_wrapper.py +59 -0
  35. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +6 -4
  36. claude_mpm/services/cli/agent_cleanup_service.py +5 -0
  37. claude_mpm/services/mcp_config_manager.py +294 -0
  38. claude_mpm/services/mcp_gateway/config/configuration.py +17 -0
  39. claude_mpm/services/mcp_gateway/main.py +38 -0
  40. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +390 -0
  41. claude_mpm/utils/log_cleanup.py +17 -17
  42. claude_mpm/utils/subprocess_utils.py +6 -6
  43. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/METADATA +24 -1
  44. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/RECORD +48 -39
  45. claude_mpm/agents/templates/agent-manager.md +0 -619
  46. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/WHEEL +0 -0
  47. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/entry_points.txt +0 -0
  48. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/licenses/LICENSE +0 -0
  49. {claude_mpm-4.3.6.dist-info → claude_mpm-4.3.12.dist-info}/top_level.txt +0 -0
@@ -15,7 +15,7 @@ DESIGN DECISIONS:
15
15
 
16
16
  import subprocess
17
17
  import sys
18
- from datetime import datetime
18
+ from datetime import datetime, timezone
19
19
  from typing import Optional
20
20
 
21
21
  from ...constants import LogLevel
@@ -68,6 +68,7 @@ def filter_claude_mpm_args(claude_args):
68
68
  "--no-native-agents",
69
69
  "--launch-method",
70
70
  "--mpm-resume",
71
+ "--reload-agents", # New flag to force rebuild system agents
71
72
  # Dependency checking flags (MPM-specific)
72
73
  "--no-check-dependencies",
73
74
  "--force-check-dependencies",
@@ -522,7 +523,7 @@ class RunCommand(BaseCommand):
522
523
  # Update session usage
523
524
  session = session_manager.load_session(resume_session_id)
524
525
  if session:
525
- session.last_used = datetime.now().isoformat()
526
+ session.last_used = datetime.now(timezone.utc).isoformat()
526
527
  session.use_count += 1
527
528
  session_manager.save_session(session)
528
529
  else:
@@ -568,6 +569,71 @@ class RunCommand(BaseCommand):
568
569
  return False
569
570
 
570
571
 
572
+ def _handle_reload_agents(logger):
573
+ """
574
+ Handle the --reload-agents flag by deleting all local claude-mpm system agents.
575
+
576
+ This forces a fresh rebuild of system agents on the next deployment,
577
+ while preserving user-created agents.
578
+
579
+ Args:
580
+ logger: Logger instance for output
581
+ """
582
+ try:
583
+ logger.info("Reloading system agents - cleaning existing deployments...")
584
+
585
+ # Import the cleanup service
586
+ from ...services.cli.agent_cleanup_service import AgentCleanupService
587
+ from ...services.agents.deployment.agent_deployment import AgentDeploymentService
588
+
589
+ # Create services
590
+ deployment_service = AgentDeploymentService()
591
+ cleanup_service = AgentCleanupService(deployment_service)
592
+
593
+ # Determine the agents directory
594
+ agents_dir = None # Will auto-detect project or user directory
595
+
596
+ # Clean deployed agents (preserves user agents)
597
+ result = cleanup_service.clean_deployed_agents(agents_dir)
598
+
599
+ # Check if cleanup was successful based on the result structure
600
+ # The service returns a dict with 'removed', 'preserved', and possibly 'errors' keys
601
+ # If it has 'success' key, use it; otherwise infer from the result
602
+ success = result.get("success", True) if "success" in result else not result.get("errors")
603
+
604
+ if success:
605
+ removed_count = result.get("cleaned_count", len(result.get("removed", [])))
606
+ removed_agents = result.get("removed", [])
607
+ preserved_agents = result.get("preserved", [])
608
+
609
+ if removed_count > 0:
610
+ logger.info(f"✅ Successfully removed {removed_count} system agents")
611
+ if removed_agents:
612
+ logger.debug(f"Removed agents: {', '.join(removed_agents)}")
613
+ print(f"🔄 Cleaned {removed_count} claude-mpm system agents")
614
+ else:
615
+ logger.info("No system agents found to clean")
616
+ print("ℹ️ No system agents found - already clean")
617
+
618
+ if preserved_agents:
619
+ logger.info(f"Preserved {len(preserved_agents)} user-created agents")
620
+ print(f"✅ Preserved {len(preserved_agents)} user-created agents")
621
+
622
+ print("🚀 System agents will be rebuilt on next use")
623
+ else:
624
+ error = result.get("error", "Cleanup failed")
625
+ if result.get("errors"):
626
+ error = f"Cleanup errors: {', '.join(result['errors'])}"
627
+ logger.error(f"Failed to clean system agents: {error}")
628
+ print(f"❌ Error cleaning agents: {error}")
629
+
630
+ except Exception as e:
631
+ logger.error(f"Error handling --reload-agents: {e}", exc_info=True)
632
+ print(f"❌ Failed to reload agents: {e}")
633
+ # Don't fail the entire session, just log the error
634
+ print("⚠️ Continuing with existing agents...")
635
+
636
+
571
637
  def run_session(args):
572
638
  """
573
639
  Main entry point for run command.
@@ -624,6 +690,10 @@ def run_session_legacy(args):
624
690
  # Check for memory usage issues with .claude.json
625
691
  _check_claude_json_memory(args, logger)
626
692
 
693
+ # Handle --reload-agents flag if specified
694
+ if getattr(args, "reload_agents", False):
695
+ _handle_reload_agents(logger)
696
+
627
697
  try:
628
698
  from ...core.claude_runner import ClaudeRunner, create_simple_context
629
699
  except ImportError:
@@ -936,7 +1006,7 @@ def run_session_legacy(args):
936
1006
  # Update session usage
937
1007
  session = session_manager.load_session(resume_session_id)
938
1008
  if session:
939
- session.last_used = datetime.now().isoformat()
1009
+ session.last_used = datetime.now(timezone.utc).isoformat()
940
1010
  session.use_count += 1
941
1011
  session_manager.save_session(session)
942
1012
  else:
@@ -0,0 +1,285 @@
1
+ """
2
+ Search command module for mcp-vector-search integration.
3
+
4
+ This module provides the /mpm-search command for semantic code search
5
+ using the mcp-vector-search service.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import sys
11
+ from typing import Any, Dict, Optional
12
+
13
+ import click
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+ from rich.table import Table
18
+
19
+ from claude_mpm.cli.utils import handle_async_errors
20
+ from claude_mpm.services.service_container import get_service_container
21
+
22
+ console = Console()
23
+
24
+
25
+ class MCPSearchInterface:
26
+ """Interface for interacting with mcp-vector-search service."""
27
+
28
+ def __init__(self):
29
+ """Initialize the search interface."""
30
+ self.container = get_service_container()
31
+ self.mcp_gateway = None
32
+
33
+ async def initialize(self):
34
+ """Initialize the MCP gateway connection."""
35
+ try:
36
+ from claude_mpm.services.mcp_gateway import MCPGatewayService
37
+ self.mcp_gateway = self.container.resolve(MCPGatewayService)
38
+ if not self.mcp_gateway:
39
+ self.mcp_gateway = MCPGatewayService()
40
+ await self.mcp_gateway.initialize()
41
+ except Exception as e:
42
+ console.print(f"[red]Failed to initialize MCP gateway: {e}[/red]")
43
+ raise
44
+
45
+ async def search_code(
46
+ self,
47
+ query: str,
48
+ limit: int = 10,
49
+ similarity_threshold: float = 0.3,
50
+ file_extensions: Optional[list] = None,
51
+ language: Optional[str] = None
52
+ ) -> Dict[str, Any]:
53
+ """Search code using semantic similarity."""
54
+ params = {
55
+ "query": query,
56
+ "limit": limit,
57
+ "similarity_threshold": similarity_threshold
58
+ }
59
+
60
+ if file_extensions:
61
+ params["file_extensions"] = file_extensions
62
+ if language:
63
+ params["language"] = language
64
+
65
+ return await self._call_mcp_tool("mcp__mcp-vector-search__search_code", params)
66
+
67
+ async def search_similar(
68
+ self,
69
+ file_path: str,
70
+ function_name: Optional[str] = None,
71
+ limit: int = 10,
72
+ similarity_threshold: float = 0.3
73
+ ) -> Dict[str, Any]:
74
+ """Find code similar to a specific file or function."""
75
+ params = {
76
+ "file_path": file_path,
77
+ "limit": limit,
78
+ "similarity_threshold": similarity_threshold
79
+ }
80
+
81
+ if function_name:
82
+ params["function_name"] = function_name
83
+
84
+ return await self._call_mcp_tool("mcp__mcp-vector-search__search_similar", params)
85
+
86
+ async def search_context(
87
+ self,
88
+ description: str,
89
+ focus_areas: Optional[list] = None,
90
+ limit: int = 10
91
+ ) -> Dict[str, Any]:
92
+ """Search for code based on contextual description."""
93
+ params = {
94
+ "description": description,
95
+ "limit": limit
96
+ }
97
+
98
+ if focus_areas:
99
+ params["focus_areas"] = focus_areas
100
+
101
+ return await self._call_mcp_tool("mcp__mcp-vector-search__search_context", params)
102
+
103
+ async def get_status(self) -> Dict[str, Any]:
104
+ """Get project indexing status and statistics."""
105
+ return await self._call_mcp_tool("mcp__mcp-vector-search__get_project_status", {})
106
+
107
+ async def index_project(
108
+ self,
109
+ force: bool = False,
110
+ file_extensions: Optional[list] = None
111
+ ) -> Dict[str, Any]:
112
+ """Index or reindex the project codebase."""
113
+ params = {"force": force}
114
+
115
+ if file_extensions:
116
+ params["file_extensions"] = file_extensions
117
+
118
+ return await self._call_mcp_tool("mcp__mcp-vector-search__index_project", params)
119
+
120
+ async def _call_mcp_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
121
+ """Call an MCP tool through the gateway."""
122
+ if not self.mcp_gateway:
123
+ await self.initialize()
124
+
125
+ try:
126
+ result = await self.mcp_gateway.call_tool(tool_name, params)
127
+ return result
128
+ except Exception as e:
129
+ return {"error": str(e)}
130
+
131
+
132
+ def display_search_results(results: Dict[str, Any], output_format: str = "rich"):
133
+ """Display search results in the specified format."""
134
+ if output_format == "json":
135
+ console.print_json(json.dumps(results, indent=2))
136
+ return
137
+
138
+ if "error" in results:
139
+ console.print(f"[red]Error: {results['error']}[/red]")
140
+ return
141
+
142
+ if not results.get("results"):
143
+ console.print("[yellow]No results found.[/yellow]")
144
+ return
145
+
146
+ # Create a table for results
147
+ table = Table(title="Search Results", show_header=True, header_style="bold magenta")
148
+ table.add_column("File", style="cyan", no_wrap=False)
149
+ table.add_column("Score", style="green", width=8)
150
+ table.add_column("Type", style="yellow", width=10)
151
+ table.add_column("Name", style="blue", no_wrap=False)
152
+
153
+ for result in results["results"]:
154
+ file_path = result.get("file_path", "Unknown")
155
+ score = f"{result.get('score', 0):.3f}"
156
+ item_type = result.get("type", "unknown")
157
+ name = result.get("name", result.get("function_name", ""))
158
+
159
+ table.add_row(file_path, score, item_type, name)
160
+
161
+ # Show snippet if available
162
+ if result.get("snippet"):
163
+ snippet_panel = Panel(
164
+ Syntax(result["snippet"], result.get("language", "python"), theme="monokai"),
165
+ title=f"[cyan]{file_path}[/cyan]",
166
+ border_style="dim"
167
+ )
168
+ console.print(snippet_panel)
169
+
170
+ console.print(table)
171
+
172
+ # Show statistics if available
173
+ if "stats" in results:
174
+ stats = results["stats"]
175
+ console.print(f"\n[bold]Statistics:[/bold]")
176
+ console.print(f" Total indexed files: {stats.get('total_files', 0)}")
177
+ console.print(f" Total indexed functions: {stats.get('total_functions', 0)}")
178
+ console.print(f" Index last updated: {stats.get('last_updated', 'Unknown')}")
179
+
180
+
181
+ @click.command()
182
+ @click.argument("query", required=False)
183
+ @click.option("--similar", "-s", help="Find code similar to a specific file")
184
+ @click.option("--context", "-c", help="Search by contextual description")
185
+ @click.option("--index", is_flag=True, help="Index or reindex the project")
186
+ @click.option("--status", is_flag=True, help="Check index status")
187
+ @click.option("--limit", "-l", default=10, help="Maximum number of results")
188
+ @click.option("--threshold", "-t", default=0.3, help="Similarity threshold (0.0-1.0)")
189
+ @click.option("--language", help="Filter by programming language")
190
+ @click.option("--extensions", multiple=True, help="Filter by file extensions")
191
+ @click.option("--function", "-f", help="Function name (with --similar)")
192
+ @click.option("--focus", multiple=True, help="Focus areas (with --context)")
193
+ @click.option("--force", is_flag=True, help="Force reindexing (with --index)")
194
+ @click.option("--json", "output_json", is_flag=True, help="Output results as JSON")
195
+ @handle_async_errors
196
+ async def search_command(
197
+ query: Optional[str],
198
+ similar: Optional[str],
199
+ context: Optional[str],
200
+ index: bool,
201
+ status: bool,
202
+ limit: int,
203
+ threshold: float,
204
+ language: Optional[str],
205
+ extensions: tuple,
206
+ function: Optional[str],
207
+ focus: tuple,
208
+ force: bool,
209
+ output_json: bool
210
+ ):
211
+ """
212
+ Search the codebase using semantic search powered by mcp-vector-search.
213
+
214
+ Examples:
215
+ /mpm-search "authentication logic"
216
+ /mpm-search --similar src/auth.py
217
+ /mpm-search --context "find all API endpoints"
218
+ /mpm-search --index --force
219
+ /mpm-search --status
220
+ """
221
+ search = MCPSearchInterface()
222
+ await search.initialize()
223
+
224
+ output_format = "json" if output_json else "rich"
225
+
226
+ try:
227
+ # Handle different operation modes
228
+ if index:
229
+ console.print("[cyan]Indexing project...[/cyan]")
230
+ result = await search.index_project(
231
+ force=force,
232
+ file_extensions=list(extensions) if extensions else None
233
+ )
234
+ if "error" not in result:
235
+ console.print("[green]✓ Project indexed successfully[/green]")
236
+ display_search_results(result, output_format)
237
+
238
+ elif status:
239
+ result = await search.get_status()
240
+ display_search_results(result, output_format)
241
+
242
+ elif similar:
243
+ result = await search.search_similar(
244
+ file_path=similar,
245
+ function_name=function,
246
+ limit=limit,
247
+ similarity_threshold=threshold
248
+ )
249
+ display_search_results(result, output_format)
250
+
251
+ elif context:
252
+ result = await search.search_context(
253
+ description=context,
254
+ focus_areas=list(focus) if focus else None,
255
+ limit=limit
256
+ )
257
+ display_search_results(result, output_format)
258
+
259
+ elif query:
260
+ result = await search.search_code(
261
+ query=query,
262
+ limit=limit,
263
+ similarity_threshold=threshold,
264
+ file_extensions=list(extensions) if extensions else None,
265
+ language=language
266
+ )
267
+ display_search_results(result, output_format)
268
+
269
+ else:
270
+ console.print("[yellow]No search operation specified. Use --help for options.[/yellow]")
271
+
272
+ except Exception as e:
273
+ console.print(f"[red]Search failed: {e}[/red]")
274
+ if not output_json:
275
+ console.print("[dim]Tip: Make sure the project is indexed with --index first[/dim]")
276
+ sys.exit(1)
277
+
278
+
279
+ def main():
280
+ """Main entry point for the search command."""
281
+ asyncio.run(search_command())
282
+
283
+
284
+ if __name__ == "__main__":
285
+ main()
@@ -199,6 +199,11 @@ def add_top_level_run_arguments(parser: argparse.ArgumentParser) -> None:
199
199
  action="store_true",
200
200
  help="Force operations even with warnings (e.g., large .claude.json file)",
201
201
  )
202
+ run_group.add_argument(
203
+ "--reload-agents",
204
+ action="store_true",
205
+ help="Force rebuild of all system agents by deleting local claude-mpm agents",
206
+ )
202
207
 
203
208
  # Dependency checking options (for backward compatibility at top level)
204
209
  dep_group_top = parser.add_argument_group(
@@ -390,6 +395,14 @@ def create_parser(
390
395
  except ImportError:
391
396
  pass
392
397
 
398
+ # Add search command parser
399
+ try:
400
+ from .search_parser import add_search_subparser
401
+
402
+ add_search_subparser(subparsers)
403
+ except ImportError:
404
+ pass
405
+
393
406
  # Import and add additional command parsers from commands module
394
407
  try:
395
408
  from ..commands.aggregate import add_aggregate_parser
@@ -177,4 +177,21 @@ def add_mcp_subparser(subparsers) -> argparse.ArgumentParser:
177
177
  help="Show setup instructions for Claude Code",
178
178
  )
179
179
 
180
+ # External MCP services management
181
+ external_mcp_parser = mcp_subparsers.add_parser(
182
+ MCPCommands.EXTERNAL.value, help="Manage external MCP services"
183
+ )
184
+ external_mcp_parser.add_argument(
185
+ "external_action",
186
+ nargs="?",
187
+ choices=["setup", "list", "check", "fix-browser", "detect"],
188
+ default="list",
189
+ help="External service action (default: list)",
190
+ )
191
+ external_mcp_parser.add_argument(
192
+ "--force",
193
+ action="store_true",
194
+ help="Force overwrite existing configuration"
195
+ )
196
+
180
197
  return mcp_parser
@@ -68,6 +68,11 @@ def add_run_arguments(parser: argparse.ArgumentParser) -> None:
68
68
  action="store_true",
69
69
  help="Force operations even with warnings (e.g., large .claude.json file)",
70
70
  )
71
+ run_group.add_argument(
72
+ "--reload-agents",
73
+ action="store_true",
74
+ help="Force rebuild of all system agents by deleting local claude-mpm agents",
75
+ )
71
76
  run_group.add_argument(
72
77
  "--mpm-resume",
73
78
  type=str,
@@ -0,0 +1,239 @@
1
+ """
2
+ Search command parser for mcp-vector-search integration.
3
+
4
+ This module provides argument parsing for the /mpm-search command.
5
+ """
6
+
7
+ import argparse
8
+ from typing import Optional
9
+
10
+
11
+ def add_search_subparser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
12
+ """
13
+ Add the search command parser.
14
+
15
+ Args:
16
+ subparsers: The subparsers action to add the search parser to.
17
+
18
+ Returns:
19
+ The created search parser.
20
+ """
21
+ search_parser = subparsers.add_parser(
22
+ "mpm-search",
23
+ aliases=["search"],
24
+ help="Search codebase using semantic search",
25
+ description=(
26
+ "Search the codebase using semantic search powered by mcp-vector-search. "
27
+ "Can search by query, find similar code, search by context, or manage the search index."
28
+ ),
29
+ formatter_class=argparse.RawDescriptionHelpFormatter,
30
+ epilog="""
31
+ Examples:
32
+ # Search for code by query
33
+ claude-mpm mpm-search "authentication logic"
34
+
35
+ # Find code similar to a file
36
+ claude-mpm mpm-search --similar src/auth.py
37
+
38
+ # Search by contextual description
39
+ claude-mpm mpm-search --context "find all API endpoints"
40
+
41
+ # Index the project (required before searching)
42
+ claude-mpm mpm-search --index
43
+
44
+ # Force reindex the project
45
+ claude-mpm mpm-search --index --force
46
+
47
+ # Check index status
48
+ claude-mpm mpm-search --status
49
+
50
+ # Search with filters
51
+ claude-mpm mpm-search "database" --language python --limit 20
52
+
53
+ # Search with multiple file extensions
54
+ claude-mpm mpm-search "test" --extensions .py --extensions .js
55
+
56
+ # Find similar code to a specific function
57
+ claude-mpm mpm-search --similar src/auth.py --function authenticate_user
58
+
59
+ # Search with context and focus areas
60
+ claude-mpm mpm-search --context "security vulnerabilities" --focus authentication --focus encryption
61
+
62
+ # Output as JSON for processing
63
+ claude-mpm mpm-search "api" --json
64
+ """,
65
+ )
66
+
67
+ # Primary search modes (mutually exclusive)
68
+ search_mode = search_parser.add_mutually_exclusive_group()
69
+
70
+ search_mode.add_argument(
71
+ "query",
72
+ nargs="?",
73
+ help="Search query for semantic code search",
74
+ )
75
+
76
+ search_mode.add_argument(
77
+ "--similar",
78
+ "-s",
79
+ metavar="FILE",
80
+ help="Find code similar to the specified file",
81
+ )
82
+
83
+ search_mode.add_argument(
84
+ "--context",
85
+ "-c",
86
+ metavar="DESCRIPTION",
87
+ help="Search by contextual description of what you're looking for",
88
+ )
89
+
90
+ # Index management options
91
+ search_parser.add_argument(
92
+ "--index",
93
+ action="store_true",
94
+ help="Index or reindex the project codebase",
95
+ )
96
+
97
+ search_parser.add_argument(
98
+ "--status",
99
+ action="store_true",
100
+ help="Check project indexing status and statistics",
101
+ )
102
+
103
+ # Search filters and options
104
+ search_parser.add_argument(
105
+ "--limit",
106
+ "-l",
107
+ type=int,
108
+ default=10,
109
+ metavar="N",
110
+ help="Maximum number of results to return (default: 10, max: 50)",
111
+ )
112
+
113
+ search_parser.add_argument(
114
+ "--threshold",
115
+ "-t",
116
+ type=float,
117
+ default=0.3,
118
+ metavar="SCORE",
119
+ help="Similarity threshold between 0.0 and 1.0 (default: 0.3)",
120
+ )
121
+
122
+ search_parser.add_argument(
123
+ "--language",
124
+ metavar="LANG",
125
+ help="Filter by programming language (e.g., python, javascript, go)",
126
+ )
127
+
128
+ search_parser.add_argument(
129
+ "--extensions",
130
+ action="append",
131
+ metavar="EXT",
132
+ help="Filter by file extensions (e.g., .py, .js). Can be specified multiple times",
133
+ )
134
+
135
+ # Options for --similar mode
136
+ search_parser.add_argument(
137
+ "--function",
138
+ "-f",
139
+ metavar="NAME",
140
+ help="Function name within the file (used with --similar)",
141
+ )
142
+
143
+ # Options for --context mode
144
+ search_parser.add_argument(
145
+ "--focus",
146
+ action="append",
147
+ metavar="AREA",
148
+ help="Focus areas for contextual search (e.g., security, performance). Can be specified multiple times",
149
+ )
150
+
151
+ # Options for --index mode
152
+ search_parser.add_argument(
153
+ "--force",
154
+ action="store_true",
155
+ help="Force reindexing even if index already exists (used with --index)",
156
+ )
157
+
158
+ # Output options
159
+ search_parser.add_argument(
160
+ "--json",
161
+ action="store_true",
162
+ dest="output_json",
163
+ help="Output results as JSON instead of formatted text",
164
+ )
165
+
166
+ # Additional filters
167
+ search_parser.add_argument(
168
+ "--class",
169
+ dest="class_name",
170
+ metavar="NAME",
171
+ help="Filter by class name",
172
+ )
173
+
174
+ search_parser.add_argument(
175
+ "--files",
176
+ metavar="PATTERN",
177
+ help="Filter by file patterns (e.g., '*.py' or 'src/*.js')",
178
+ )
179
+
180
+ # Verbose output
181
+ search_parser.add_argument(
182
+ "-v",
183
+ "--verbose",
184
+ action="store_true",
185
+ help="Enable verbose output with additional details",
186
+ )
187
+
188
+ return search_parser
189
+
190
+
191
+ def validate_search_args(args: argparse.Namespace) -> Optional[str]:
192
+ """
193
+ Validate search command arguments.
194
+
195
+ Args:
196
+ args: Parsed command line arguments.
197
+
198
+ Returns:
199
+ Error message if validation fails, None otherwise.
200
+ """
201
+ # Check threshold is in valid range
202
+ if hasattr(args, "threshold") and args.threshold is not None:
203
+ if not 0.0 <= args.threshold <= 1.0:
204
+ return "Similarity threshold must be between 0.0 and 1.0"
205
+
206
+ # Check limit is reasonable
207
+ if hasattr(args, "limit") and args.limit is not None:
208
+ if args.limit < 1:
209
+ return "Limit must be at least 1"
210
+ if args.limit > 50:
211
+ return "Limit cannot exceed 50"
212
+
213
+ # Check that function is only used with --similar
214
+ if hasattr(args, "function") and args.function and not getattr(args, "similar", None):
215
+ return "--function can only be used with --similar"
216
+
217
+ # Check that focus is only used with --context
218
+ if hasattr(args, "focus") and args.focus and not getattr(args, "context", None):
219
+ return "--focus can only be used with --context"
220
+
221
+ # Check that force is only used with --index
222
+ if hasattr(args, "force") and args.force and not getattr(args, "index", False):
223
+ return "--force can only be used with --index"
224
+
225
+ # Ensure at least one operation is specified
226
+ if hasattr(args, "command") and args.command in ["mpm-search", "search"]:
227
+ has_operation = any(
228
+ [
229
+ getattr(args, "query", None),
230
+ getattr(args, "similar", None),
231
+ getattr(args, "context", None),
232
+ getattr(args, "index", False),
233
+ getattr(args, "status", False),
234
+ ]
235
+ )
236
+ if not has_operation:
237
+ return "No search operation specified. Use --help for options."
238
+
239
+ return None