mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48e6e84beb | |||
| 02c14acddb | |||
| 0b5425f163 | |||
| 0bcda4a14a |
@@ -1,5 +1,20 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.17.2 (2025-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Allow recent_activity discovery mode in cloud mode
|
||||
([`0bcda4a`](https://github.com/basicmachines-co/basic-memory/commit/0bcda4a))
|
||||
- Add `allow_discovery` parameter to `resolve_project_parameter()`
|
||||
- Tools like `recent_activity` can now work across all projects in cloud mode
|
||||
- Fix circular import in project_context module
|
||||
|
||||
### Internal
|
||||
|
||||
- Optimize release workflow by running lint/typecheck only (skip full tests)
|
||||
([`0b5425f`](https://github.com/basicmachines-co/basic-memory/commit/0b5425f))
|
||||
|
||||
## v0.17.1 (2025-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -182,8 +182,9 @@ release version:
|
||||
fi
|
||||
|
||||
# Run quality checks
|
||||
echo "🔍 Running quality checks..."
|
||||
just check
|
||||
echo "🔍 Running lint checks..."
|
||||
just lint
|
||||
just typecheck
|
||||
|
||||
# Update version in __init__.py
|
||||
echo "📝 Updating version in __init__.py..."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
# Package version - updated by release automation
|
||||
__version__ = "0.17.1"
|
||||
__version__ = "0.17.2"
|
||||
|
||||
# API version for FastAPI - independent of package version
|
||||
__api_version__ = "v0"
|
||||
|
||||
@@ -14,16 +14,17 @@ from loguru import logger
|
||||
from fastmcp import Context
|
||||
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
from basic_memory.schemas.project_info import ProjectItem, ProjectList
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
async def resolve_project_parameter(project: Optional[str] = None) -> Optional[str]:
|
||||
async def resolve_project_parameter(
|
||||
project: Optional[str] = None, allow_discovery: bool = False
|
||||
) -> Optional[str]:
|
||||
"""Resolve project parameter using three-tier hierarchy.
|
||||
|
||||
if config.cloud_mode:
|
||||
project is required
|
||||
project is required (unless allow_discovery=True for tools that support discovery mode)
|
||||
else:
|
||||
Resolution order:
|
||||
1. Single Project Mode (--project cli arg, or BASIC_MEMORY_MCP_PROJECT env var) - highest priority
|
||||
@@ -32,17 +33,22 @@ async def resolve_project_parameter(project: Optional[str] = None) -> Optional[s
|
||||
|
||||
Args:
|
||||
project: Optional explicit project parameter
|
||||
allow_discovery: If True, allows returning None in cloud mode for discovery mode
|
||||
(used by tools like recent_activity that can operate across all projects)
|
||||
|
||||
Returns:
|
||||
Resolved project name or None if no resolution possible
|
||||
"""
|
||||
|
||||
config = ConfigManager().config
|
||||
# if cloud_mode, project is required
|
||||
# if cloud_mode, project is required (unless discovery mode is allowed)
|
||||
if config.cloud_mode:
|
||||
if project:
|
||||
logger.debug(f"project: {project}, cloud_mode: {config.cloud_mode}")
|
||||
return project
|
||||
elif allow_discovery:
|
||||
logger.debug("cloud_mode: discovery mode allowed, returning None")
|
||||
return None
|
||||
else:
|
||||
raise ValueError("No project specified. Project is required for cloud mode.")
|
||||
|
||||
@@ -67,6 +73,9 @@ async def resolve_project_parameter(project: Optional[str] = None) -> Optional[s
|
||||
|
||||
|
||||
async def get_project_names(client: AsyncClient, headers: HeaderTypes | None = None) -> List[str]:
|
||||
# Deferred import to avoid circular dependency with tools
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
|
||||
response = await call_get(client, "/projects/projects", headers=headers)
|
||||
project_list = ProjectList.model_validate(response.json())
|
||||
return [project.name for project in project_list.projects]
|
||||
@@ -92,6 +101,9 @@ async def get_active_project(
|
||||
ValueError: If no project can be resolved
|
||||
HTTPError: If project doesn't exist or is inaccessible
|
||||
"""
|
||||
# Deferred import to avoid circular dependency with tools
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
|
||||
resolved_project = await resolve_project_parameter(project)
|
||||
if not resolved_project:
|
||||
project_names = await get_project_names(client, headers)
|
||||
|
||||
@@ -135,7 +135,8 @@ async def recent_activity(
|
||||
params["type"] = [t.value for t in validated_types] # pyright: ignore
|
||||
|
||||
# Resolve project parameter using the three-tier hierarchy
|
||||
resolved_project = await resolve_project_parameter(project)
|
||||
# allow_discovery=True enables Discovery Mode, so a project is not required
|
||||
resolved_project = await resolve_project_parameter(project, allow_discovery=True)
|
||||
|
||||
if resolved_project is None:
|
||||
# Discovery Mode: Get activity across all projects
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Tests for project context utilities."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestResolveProjectParameter:
|
||||
"""Tests for resolve_project_parameter function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_mode_requires_project_by_default(self):
|
||||
"""In cloud mode, project is required when allow_discovery=False."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = True
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await resolve_project_parameter(project=None, allow_discovery=False)
|
||||
|
||||
assert "No project specified" in str(exc_info.value)
|
||||
assert "Project is required for cloud mode" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_mode_allows_discovery_when_enabled(self):
|
||||
"""In cloud mode with allow_discovery=True, returns None instead of error."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = True
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
result = await resolve_project_parameter(project=None, allow_discovery=True)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_mode_returns_project_when_specified(self):
|
||||
"""In cloud mode, returns the specified project."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = True
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
result = await resolve_project_parameter(project="my-project")
|
||||
|
||||
assert result == "my-project"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mode_uses_env_var_priority(self):
|
||||
"""In local mode, BASIC_MEMORY_MCP_PROJECT env var takes priority."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = False
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
with patch.dict(os.environ, {"BASIC_MEMORY_MCP_PROJECT": "env-project"}):
|
||||
result = await resolve_project_parameter(project="explicit-project")
|
||||
|
||||
# Env var should take priority over explicit project
|
||||
assert result == "env-project"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mode_uses_explicit_project(self):
|
||||
"""In local mode without env var, uses explicit project parameter."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = False
|
||||
mock_config.default_project_mode = False
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Remove the env var if it exists
|
||||
os.environ.pop("BASIC_MEMORY_MCP_PROJECT", None)
|
||||
result = await resolve_project_parameter(project="explicit-project")
|
||||
|
||||
assert result == "explicit-project"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mode_uses_default_project(self):
|
||||
"""In local mode with default_project_mode, uses default project."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = False
|
||||
mock_config.default_project_mode = True
|
||||
mock_config.default_project = "default-project"
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
os.environ.pop("BASIC_MEMORY_MCP_PROJECT", None)
|
||||
result = await resolve_project_parameter(project=None)
|
||||
|
||||
assert result == "default-project"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mode_returns_none_when_no_resolution(self):
|
||||
"""In local mode without any project source, returns None."""
|
||||
from basic_memory.mcp.project_context import resolve_project_parameter
|
||||
|
||||
mock_config = MagicMock()
|
||||
mock_config.cloud_mode = False
|
||||
mock_config.default_project_mode = False
|
||||
|
||||
with patch(
|
||||
"basic_memory.mcp.project_context.ConfigManager"
|
||||
) as mock_config_manager:
|
||||
mock_config_manager.return_value.config = mock_config
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
os.environ.pop("BASIC_MEMORY_MCP_PROJECT", None)
|
||||
result = await resolve_project_parameter(project=None)
|
||||
|
||||
assert result is None
|
||||
Reference in New Issue
Block a user