Files
basicmachines-co-basic-memory/src/basic_memory/project_resolver.py
T
2026-02-16 21:42:25 -06:00

122 lines
3.7 KiB
Python

"""Unified project resolution across MCP, API, and CLI."""
import os
from dataclasses import dataclass
from enum import Enum, auto
from typing import Optional
from loguru import logger
class ResolutionMode(Enum):
"""How the project was resolved."""
ENV_CONSTRAINT = auto() # BASIC_MEMORY_MCP_PROJECT env var
EXPLICIT = auto() # Explicit project parameter
DEFAULT = auto() # default_project from config
DISCOVERY = auto() # Discovery mode allowed (no project)
NONE = auto() # No resolution possible
@dataclass(frozen=True)
class ResolvedProject:
"""Result of project resolution."""
project: Optional[str]
mode: ResolutionMode
reason: str
@property
def is_resolved(self) -> bool:
return self.project is not None
@property
def is_discovery_mode(self) -> bool:
return self.mode in {ResolutionMode.DISCOVERY, ResolutionMode.NONE} and self.project is None
@dataclass
class ProjectResolver:
"""Unified project resolution logic."""
default_project: Optional[str] = None
constrained_project: Optional[str] = None
@classmethod
def from_env(
cls,
default_project: Optional[str] = None,
) -> "ProjectResolver":
"""Create resolver with constrained_project from environment."""
return cls(
default_project=default_project,
constrained_project=os.environ.get("BASIC_MEMORY_MCP_PROJECT"),
)
def resolve(
self,
project: Optional[str] = None,
allow_discovery: bool = False,
) -> ResolvedProject:
"""Resolve project using a unified linear priority chain."""
if self.constrained_project:
logger.debug(f"Using constrained project from env: {self.constrained_project}")
return ResolvedProject(
project=self.constrained_project,
mode=ResolutionMode.ENV_CONSTRAINT,
reason=f"Environment constraint: BASIC_MEMORY_MCP_PROJECT={self.constrained_project}",
)
if project:
logger.debug(f"Using explicit project parameter: {project}")
return ResolvedProject(
project=project,
mode=ResolutionMode.EXPLICIT,
reason=f"Explicit parameter: {project}",
)
if self.default_project:
logger.debug(f"Using default project from config: {self.default_project}")
return ResolvedProject(
project=self.default_project,
mode=ResolutionMode.DEFAULT,
reason=f"Default project: {self.default_project}",
)
if allow_discovery:
logger.debug("No project resolved, using discovery mode")
return ResolvedProject(
project=None,
mode=ResolutionMode.DISCOVERY,
reason="Discovery mode enabled",
)
logger.debug("No project resolution possible")
return ResolvedProject(
project=None,
mode=ResolutionMode.NONE,
reason="No project specified and no default project configured",
)
def require_project(
self,
project: Optional[str] = None,
error_message: Optional[str] = None,
) -> ResolvedProject:
"""Resolve project, raising an error if not resolved."""
result = self.resolve(project, allow_discovery=False)
if not result.is_resolved:
msg = error_message or (
"No project specified. Either set 'default_project' in config, "
"or provide a 'project' argument."
)
raise ValueError(msg)
return result
__all__ = [
"ProjectResolver",
"ResolvedProject",
"ResolutionMode",
]