Compare commits

...

4 Commits

Author SHA1 Message Date
phernandez 48e6e84beb chore: update version to 0.17.2 for v0.17.2 release 2025-12-29 16:47:04 -06:00
phernandez 02c14acddb docs: add CHANGELOG entry for v0.17.2
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 16:46:26 -06:00
phernandez 0b5425f163 don't run full tests in release, just lint, typecheck
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 16:33:38 -06:00
phernandez 0bcda4a14a fix: allow recent_activity discovery mode in cloud mode
Add `allow_discovery` parameter to `resolve_project_parameter()` that
enables tools like `recent_activity` to work across all projects in
cloud mode without requiring a project parameter.

- Add `allow_discovery: bool = False` param to resolve_project_parameter
- In cloud mode with allow_discovery=True, return None instead of error
- Update recent_activity to use allow_discovery=True
- Fix circular import by deferring call_get import inside functions
- Add comprehensive tests for resolve_project_parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-12-29 16:22:48 -06:00
6 changed files with 181 additions and 8 deletions
+15
View File
@@ -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
+3 -2
View File
@@ -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 -1
View File
@@ -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"
+16 -4
View File
@@ -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
+144
View File
@@ -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