mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
0239f4abb4
Signed-off-by: phernandez <paul@basicmachines.co>
122 lines
3.7 KiB
Python
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",
|
|
]
|