Skip to main content
Glama

Mode Manager MCP

instruction_manager.py20.4 kB
""" Mode Manager for VS Code .instructions.md files. This module handles instruction files which define custom instructions and workspace-specific AI guidance for VS Code Copilot. Note: This file has been refactored to eliminate DRY violations. """ import json import logging import os from pathlib import Path from typing import Any, Dict, List, Optional, Union from urllib.parse import unquote from .path_utils import get_vscode_prompts_directory from .simple_file_ops import ( FileOperationError, parse_frontmatter, parse_frontmatter_file, safe_delete_file, write_frontmatter_file, ) from .types import LanguagePattern, MemoryScope logger = logging.getLogger(__name__) INSTRUCTION_FILE_EXTENSION = ".instructions.md" class MemoryFileConfig: """Configuration for memory file creation.""" def __init__(self, scope: MemoryScope, language: Optional[str] = None): self.scope = scope self.language = language @property def filename(self) -> str: """Generate the appropriate filename for the memory file.""" if self.language: return f"memory-{self.language.lower()}{INSTRUCTION_FILE_EXTENSION}" return f"memory{INSTRUCTION_FILE_EXTENSION}" @property def description(self) -> str: """Generate the appropriate description for the memory file.""" if self.language: return f"Personal AI memory for {self.language} development" elif self.scope == MemoryScope.workspace: return "Workspace-specific AI memory for this project" else: return "Personal AI memory for conversations and preferences" @property def initial_content(self) -> str: """Generate the initial content for the memory file.""" title = f"# {'Workspace' if self.scope == MemoryScope.workspace else 'Personal'} AI Memory" if self.language: title += f" - {self.language.title()}" description = f"\nThis file contains {'workspace-specific' if self.scope == MemoryScope.workspace else 'personal'} information for AI conversations." if self.language: description += f" Specifically for {self.language} development." return title + description + "\n\n## Memories\n" class InstructionManager: """Manages VS Code .instructions.md files in both user and workspace prompts directories.""" def __init__(self, prompts_dir: Optional[Union[str, Path]] = None): """ Initialize instruction manager. Args: prompts_dir: Custom prompts directory (default: VS Code user dir + prompts) """ if prompts_dir: self.prompts_dir = Path(prompts_dir) else: self.prompts_dir = get_vscode_prompts_directory() # Ensure prompts directory exists self.prompts_dir.mkdir(parents=True, exist_ok=True) # Workspace instructions directory (current working directory + .github/instructions) self.workspace_prompts_dir = Path(os.getcwd()) / ".github" / "instructions" logger.info(f"Instruction manager initialized with prompts directory: {self.prompts_dir}") logger.info(f"Workspace instructions directory: {self.workspace_prompts_dir}") def _decode_workspace_root(self, workspace_root: Optional[str]) -> Optional[str]: """Decode workspace root URL if needed.""" if workspace_root is None: return None return unquote(workspace_root) if "%" in workspace_root else workspace_root def _ensure_instruction_extension(self, filename: str) -> str: """Ensure filename has the correct .instructions.md extension.""" return filename if filename.endswith(INSTRUCTION_FILE_EXTENSION) else filename + INSTRUCTION_FILE_EXTENSION def _build_workspace_instructions_path(self, workspace_root: str) -> Path: """Build workspace instructions directory path.""" decoded_root = self._decode_workspace_root(workspace_root) if decoded_root is None: raise ValueError("Workspace root cannot be None") return Path(decoded_root) / ".github" / "instructions" def _get_prompts_dir(self, scope: MemoryScope = MemoryScope.user, workspace_root: Optional[str] = None) -> Path: """Get the appropriate prompts directory based on scope.""" if scope == MemoryScope.workspace: if workspace_root: return self._build_workspace_instructions_path(workspace_root) return self.workspace_prompts_dir return self.prompts_dir def _ensure_workspace_instructions_dir(self, workspace_root: Optional[str] = None) -> None: """Ensure workspace instructions directory exists.""" if workspace_root: workspace_dir = self._build_workspace_instructions_path(workspace_root) workspace_dir.mkdir(parents=True, exist_ok=True) else: self.workspace_prompts_dir.mkdir(parents=True, exist_ok=True) def _get_apply_to_pattern(self, language: Optional[str] = None) -> str: """Get the appropriate applyTo pattern based on language.""" if not language: return LanguagePattern.get_all_pattern() return LanguagePattern.get_pattern(language) def create_memory( self, memory_item: str, scope: MemoryScope = MemoryScope.user, language: Optional[str] = None, workspace_root: Optional[str] = None, ) -> Dict[str, Any]: """ Create or append to a memory instruction file. Args: memory_item: The memory item to store scope: "user" or "workspace" language: Optional language for language-specific memory workspace_root: Optional workspace root path (for workspace scope) Returns: Dict with operation result details Raises: FileOperationError: If operation fails """ if scope == MemoryScope.workspace: if workspace_root is None: raise FileOperationError("Workspace root is required for workspace scope memory operations") # Use the provided workspace root, URL-decoded in case it comes from a FileUrl self.workspace_prompts_dir = self._build_workspace_instructions_path(workspace_root) self._ensure_workspace_instructions_dir(workspace_root) prompts_dir = self._get_prompts_dir(scope, workspace_root) apply_to_pattern = self._get_apply_to_pattern(language) # Use MemoryFileConfig to handle file configuration config = MemoryFileConfig(scope, language) filename = config.filename description = config.description file_path = prompts_dir / filename # Create file if it doesn't exist if not file_path.exists(): initial_content = config.initial_content frontmatter = {"applyTo": apply_to_pattern, "description": description} success = write_frontmatter_file(file_path, frontmatter, initial_content, create_backup=False) if not success: raise FileOperationError(f"Failed to create memory file: {filename}") # Append the memory item import datetime timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") new_memory_entry = f"- **{timestamp}:** {memory_item}\n" success = self.append_to_section(filename, "Memories", new_memory_entry, scope, workspace_root) if not success: raise FileOperationError(f"Failed to append memory to: {filename}") return { "status": "success", "filename": filename, "scope": scope, "language": language, "path": str(file_path), "apply_to": apply_to_pattern, } async def create_memory_with_optimization( self, memory_item: str, ctx: Any, # FastMCP Context scope: MemoryScope = MemoryScope.user, language: Optional[str] = None, workspace_root: Optional[str] = None, ) -> Dict[str, Any]: """ Enhanced create_memory that includes smart optimization. Fully backward compatible with existing memory files. """ # First, create/append memory using existing logic result = self.create_memory(memory_item, scope, language, workspace_root) if result["status"] == "success": # Try to optimize if needed from .memory_optimizer import MemoryOptimizer file_path = Path(result["path"]) optimizer = MemoryOptimizer(self) optimization_result = await optimizer.optimize_memory_if_needed(file_path, ctx) # Add optimization info to result result["optimization"] = optimization_result # Update success message based on optimization outcome if optimization_result["status"] == "optimized": result["message"] = f"Memory added and optimized: {memory_item}" elif optimization_result["status"] == "metadata_updated": result["message"] = f"Memory added with metadata update: {memory_item}" else: result["message"] = f"Memory added: {memory_item}" return result def append_to_section( self, instruction_name: str, section_header: str, new_entry: str, scope: MemoryScope = MemoryScope.user, workspace_root: Optional[str] = None, ) -> bool: """ Append a new entry to the end of an instruction file (fast append). Args: instruction_name: Name of the .instructions.md file section_header: Ignored (kept for compatibility) new_entry: Content to append (should include any formatting, e.g., '- ...') scope: "user" or "workspace" to determine which directory to use workspace_root: Optional workspace root path (for workspace scope) Returns: True if successful Raises: FileOperationError: If file cannot be updated """ instruction_name = self._ensure_instruction_extension(instruction_name) prompts_dir = self._get_prompts_dir(scope, workspace_root) file_path = prompts_dir / instruction_name if not file_path.exists(): raise FileOperationError(f"Instruction file not found: {instruction_name}") try: with open(file_path, "a", encoding="utf-8") as f: # Ensure entry ends with a newline entry = new_entry if new_entry.endswith("\n") else new_entry + "\n" f.write(entry) logger.info(f"Appended entry to end of: {file_path}") return True except Exception as e: raise FileOperationError(f"Error appending entry to {instruction_name}: {e}") def list_instructions(self, scope: MemoryScope = MemoryScope.user) -> List[Dict[str, Any]]: """ List all .instructions.md files in the prompts directory. Args: scope: "user" or "workspace" to determine which directory to list Returns: List of instruction file information """ instructions: List[Dict[str, Any]] = [] prompts_dir = self._get_prompts_dir(scope) if not prompts_dir.exists(): return instructions for file_path in prompts_dir.glob(f"*{INSTRUCTION_FILE_EXTENSION}"): try: frontmatter, content = parse_frontmatter_file(file_path) # Get preview of content (first 100 chars) content_preview = content.strip()[:100] if content.strip() else "" instruction_info = { "filename": file_path.name, "name": file_path.name.replace(INSTRUCTION_FILE_EXTENSION, ""), "path": str(file_path), "description": frontmatter.get("description", ""), "frontmatter": frontmatter, "content_preview": content_preview, "size": file_path.stat().st_size, "modified": file_path.stat().st_mtime, "scope": scope, } instructions.append(instruction_info) except Exception as e: logger.warning(f"Error reading instruction file {file_path}: {e}") continue # Sort by name instructions.sort(key=lambda x: x["name"].lower()) return instructions def get_instruction(self, instruction_name: str, scope: MemoryScope = MemoryScope.user) -> Dict[str, Any]: """ Get content and metadata of a specific instruction file. Args: instruction_name: Name of the .instructions.md file scope: "user" or "workspace" to determine which directory to use Returns: Instruction data including frontmatter and content Raises: FileOperationError: If file cannot be read """ # Ensure filename has correct extension instruction_name = self._ensure_instruction_extension(instruction_name) prompts_dir = self._get_prompts_dir(scope) file_path = prompts_dir / instruction_name if not file_path.exists(): raise FileOperationError(f"Instruction file not found: {instruction_name}") try: frontmatter, content = parse_frontmatter_file(file_path) return { "instruction_name": instruction_name, "name": instruction_name.replace(INSTRUCTION_FILE_EXTENSION, ""), "path": str(file_path), "description": frontmatter.get("description", ""), "frontmatter": frontmatter, "content": content, "size": file_path.stat().st_size, "modified": file_path.stat().st_mtime, "scope": scope, } except Exception as e: raise FileOperationError(f"Error reading instruction file {instruction_name}: {e}") def get_raw_instruction(self, instruction_name: str, scope: MemoryScope = MemoryScope.user) -> str: """ Get the raw file content of a specific instruction file without any processing. Args: instruction_name: Name of the .instructions.md file scope: "user" or "workspace" to determine which directory to use Returns: Raw file content as string Raises: FileOperationError: If file cannot be read """ # Ensure filename has correct extension instruction_name = self._ensure_instruction_extension(instruction_name) prompts_dir = self._get_prompts_dir(scope) file_path = prompts_dir / instruction_name if not file_path.exists(): raise FileOperationError(f"Instruction file not found: {instruction_name}") try: with open(file_path, "r", encoding="utf-8") as f: return f.read() except Exception as e: raise FileOperationError(f"Error reading raw instruction file {instruction_name}: {e}") def create_instruction(self, instruction_name: str, description: str, content: str) -> bool: """ Create a new instruction file. Args: instruction_name: Name for the new .instructions.md file description: Description of the instruction content: Instruction content Returns: True if successful Raises: FileOperationError: If file cannot be created """ # Ensure filename has correct extension instruction_name = self._ensure_instruction_extension(instruction_name) file_path = self.prompts_dir / instruction_name if file_path.exists(): raise FileOperationError(f"Instruction file already exists: {instruction_name}") # Create frontmatter with applyTo field so instructions are actually applied frontmatter: Dict[str, Any] = {"applyTo": "**", "description": description} try: success = write_frontmatter_file(file_path, frontmatter, content, create_backup=False) if success: logger.info(f"Created instruction file: {instruction_name}") return success except Exception as e: raise FileOperationError(f"Error creating instruction file {instruction_name}: {e}") def update_instruction( self, instruction_name: str, frontmatter: Optional[Dict[str, Any]] = None, content: Optional[str] = None, ) -> bool: """ Replace the content and/or frontmatter of an instruction file. This method is for full rewrites. To append to a section, use append_to_section. Args: instruction_name: Name of the .instructions.md file frontmatter: New frontmatter (optional) content: New content (optional, replaces all markdown content) Returns: True if successful Raises: FileOperationError: If file cannot be updated """ # Ensure filename has correct extension instruction_name = self._ensure_instruction_extension(instruction_name) file_path = self.prompts_dir / instruction_name if not file_path.exists(): raise FileOperationError(f"Instruction file not found: {instruction_name}") try: # Read current content current_frontmatter, current_content = parse_frontmatter_file(file_path) if content is not None and frontmatter is None: # We check if the content is actually including yaml frontmatter, content = parse_frontmatter(content) # Use provided values or keep current ones new_frontmatter = frontmatter if frontmatter is not None else current_frontmatter # If new content is provided, replace all markdown content if content is not None: new_content = content else: new_content = current_content success = write_frontmatter_file(file_path, new_frontmatter, new_content, create_backup=True) if success: logger.info(f"Updated instruction file with backup: {instruction_name}") return success except Exception as e: raise FileOperationError(f"Error updating instruction file {instruction_name}: {e}") def delete_instruction(self, instruction_name: str) -> bool: """ Delete an instruction file with automatic backup. Args: instruction_name: Name of the .instructions.md file Returns: True if successful Raises: FileOperationError: If file cannot be deleted """ # Ensure filename has correct extension instruction_name = self._ensure_instruction_extension(instruction_name) file_path = self.prompts_dir / instruction_name if not file_path.exists(): raise FileOperationError(f"Instruction file not found: {instruction_name}") try: # Use safe delete which creates backup automatically safe_delete_file(file_path, create_backup=True) logger.info(f"Deleted instruction file with backup: {instruction_name}") return True except Exception as e: raise FileOperationError(f"Error deleting instruction file {instruction_name}: {e}") def get_memory_file_path(self, scope: MemoryScope = MemoryScope.user, language: Optional[str] = None, workspace_root: Optional[str] = None) -> Path: """ Get the path to a memory file. Args: scope: Memory scope (user or workspace) language: Optional language for language-specific memory workspace_root: Optional workspace root path (for workspace scope) Returns: Path to the memory file """ prompts_dir = self._get_prompts_dir(scope, workspace_root) config = MemoryFileConfig(scope, language) return prompts_dir / config.filename

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NiclasOlofsson/mode-manager-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server