Skip to content

Commit deef89a

Browse files
jope-bmclaudephernandezgithub-actions[bot]
authored
feat: Add display_name and is_private to ProjectItem (#574)
Signed-off-by: Joe P <joe@basicmemory.com> Signed-off-by: phernandez <paul@basicmachines.co> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com> Co-authored-by: phernandez <paul@basicmachines.co> Co-authored-by: jope-bm <jope-bm@users.noreply.github.com>
1 parent 30499a9 commit deef89a

13 files changed

Lines changed: 256 additions & 18 deletions

src/basic_memory/config.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -688,11 +688,8 @@ def add_project(self, name: str, path: str) -> ProjectConfig:
688688
if project_name: # pragma: no cover
689689
raise ValueError(f"Project '{name}' already exists")
690690

691-
# Ensure the path exists
692-
project_path = Path(path)
693-
project_path.mkdir(parents=True, exist_ok=True) # pragma: no cover
694-
695691
# Load config, modify it, and save it
692+
project_path = Path(path)
696693
config = self.load_config()
697694
config.projects[name] = ProjectEntry(path=str(project_path))
698695
self.save_config(config)

src/basic_memory/deps/services.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import asyncio
1111
import os
12+
from pathlib import Path
1213
from typing import Annotated, Any, Callable, Coroutine, Mapping, Protocol
1314

1415
from fastapi import Depends
@@ -549,9 +550,15 @@ async def _reindex_project(**_: Any) -> None:
549550

550551
async def get_project_service(
551552
project_repository: ProjectRepositoryDep,
553+
app_config: AppConfigDep,
552554
) -> ProjectService:
553-
"""Create ProjectService with repository."""
554-
return ProjectService(repository=project_repository)
555+
"""Create ProjectService with repository and a system-level FileService for directory operations."""
556+
# A system-level FileService for project directory creation (no project-specific base_path needed).
557+
# ensure_directory() accepts absolute paths and ignores base_path for those, so Path.home() is safe.
558+
entity_parser = EntityParser(Path.home())
559+
markdown_processor = MarkdownProcessor(entity_parser, app_config=app_config)
560+
file_service = FileService(Path.home(), markdown_processor, app_config=app_config)
561+
return ProjectService(repository=project_repository, file_service=file_service)
555562

556563

557564
ProjectServiceDep = Annotated[ProjectService, Depends(get_project_service)]

src/basic_memory/mcp/tools/project_management.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,12 @@ async def list_memory_projects(
6464

6565
result = "Available projects:\n"
6666
for project in project_list.projects:
67-
result += f"• {project.name}\n"
67+
label = (
68+
f"{project.display_name} ({project.name})"
69+
if project.display_name
70+
else project.name
71+
)
72+
result += f"• {label}\n"
6873

6974
result += "\n" + "─" * 40 + "\n"
7075
result += "Next: Ask which project to use for this session.\n"

src/basic_memory/repository/fastembed_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
1010

1111
if TYPE_CHECKING:
12-
from fastembed import TextEmbedding # pragma: no cover
12+
from fastembed import TextEmbedding # type: ignore[import-not-found] # pragma: no cover
1313

1414

1515
class FastEmbedEmbeddingProvider(EmbeddingProvider):
@@ -42,7 +42,7 @@ async def _load_model(self) -> "TextEmbedding":
4242

4343
def _create_model() -> "TextEmbedding":
4444
try:
45-
from fastembed import TextEmbedding
45+
from fastembed import TextEmbedding # type: ignore[import-not-found]
4646
except (
4747
ImportError
4848
) as exc: # pragma: no cover - exercised via tests with monkeypatch

src/basic_memory/repository/openai_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def _get_client(self) -> Any:
4141
return self._client
4242

4343
try:
44-
from openai import AsyncOpenAI
44+
from openai import AsyncOpenAI # type: ignore[import-not-found]
4545
except ImportError as exc: # pragma: no cover - covered via monkeypatch tests
4646
raise SemanticDependenciesMissingError(
4747
"OpenAI dependency is missing. "

src/basic_memory/repository/sqlite_search_repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ async def _ensure_sqlite_vec_loaded(self, session) -> None:
343343
pass
344344

345345
try:
346-
import sqlite_vec
346+
import sqlite_vec # type: ignore[import-not-found]
347347
except ImportError as exc:
348348
raise SemanticDependenciesMissingError(
349349
"sqlite-vec package is missing. "

src/basic_memory/schemas/project_info.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ class ProjectItem(BaseModel):
178178
name: str
179179
path: str
180180
is_default: bool = False
181+
# Optional metadata injected by cloud hosting layer (not stored in DB)
182+
display_name: Optional[str] = None
183+
is_private: bool = False
181184

182185
@property
183186
def permalink(self) -> str: # pragma: no cover

src/basic_memory/services/file_service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class FileService:
4343
def __init__(
4444
self,
4545
base_path: Path,
46-
markdown_processor: MarkdownProcessor,
46+
markdown_processor: Optional[MarkdownProcessor] = None,
4747
max_concurrent_files: int = 10,
4848
app_config: Optional["BasicMemoryConfig"] = None,
4949
):
@@ -79,6 +79,10 @@ async def read_entity_content(self, entity: EntityModel) -> str:
7979
"""
8080
logger.debug(f"Reading entity content, entity_id={entity.id}, permalink={entity.permalink}")
8181

82+
# markdown_processor is required for entity content reads — fail fast if not configured
83+
if self.markdown_processor is None:
84+
raise ValueError("markdown_processor is required for read_entity_content")
85+
8286
file_path = self.get_entity_path(entity)
8387
markdown = await self.markdown_processor.read_file(file_path)
8488
return markdown.content or ""

src/basic_memory/services/project_service.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import shutil
77
from datetime import datetime
88
from pathlib import Path
9-
from typing import Dict, Optional, Sequence
9+
from typing import TYPE_CHECKING, Dict, Optional, Sequence
1010

1111

1212
from loguru import logger
@@ -30,16 +30,22 @@
3030
)
3131
from basic_memory.utils import generate_permalink
3232

33+
if TYPE_CHECKING: # pragma: no cover
34+
from basic_memory.services.file_service import FileService
35+
3336

3437
class ProjectService:
3538
"""Service for managing Basic Memory projects."""
3639

3740
repository: ProjectRepository
3841

39-
def __init__(self, repository: ProjectRepository):
42+
def __init__(
43+
self, repository: ProjectRepository, file_service: Optional["FileService"] = None
44+
):
4045
"""Initialize the project service."""
4146
super().__init__()
4247
self.repository = repository
48+
self.file_service = file_service
4349

4450
@property
4551
def config_manager(self) -> ConfigManager:
@@ -205,6 +211,16 @@ async def add_project(self, name: str, path: str, set_default: bool = False) ->
205211
f"Projects cannot share directory trees."
206212
)
207213

214+
# Ensure the project directory exists on disk.
215+
# Trigger: project_root not set means local filesystem mode (not S3/cloud)
216+
# Why: FileService (or future S3FileService) provides cloud-compatible directory creation;
217+
# direct Path.mkdir() bypasses this abstraction
218+
# Outcome: directory exists before config/DB entries are written
219+
if not self.config_manager.config.project_root:
220+
if self.file_service is None:
221+
raise ValueError("file_service is required for local project directory creation")
222+
await self.file_service.ensure_directory(Path(resolved_path))
223+
208224
# First add to config file (this validates project uniqueness and keeps
209225
# config + database aligned for all backends).
210226
self.config_manager.add_project(name, resolved_path)
@@ -459,8 +475,14 @@ async def move_project(self, name: str, new_path: str) -> None:
459475
if name not in self.config_manager.projects:
460476
raise ValueError(f"Project '{name}' not found in configuration")
461477

462-
# Create the new directory if it doesn't exist
463-
Path(resolved_path).mkdir(parents=True, exist_ok=True)
478+
# Create the new directory if it doesn't exist (skip in cloud mode where storage is S3)
479+
# Trigger: project_root not set means local filesystem mode
480+
# Why: FileService (or future S3FileService) provides cloud-compatible directory creation
481+
# Outcome: destination directory exists before config/DB are updated
482+
if not self.config_manager.config.project_root:
483+
if self.file_service is None:
484+
raise ValueError("file_service is required for local project directory creation")
485+
await self.file_service.ensure_directory(Path(resolved_path))
464486

465487
# Update in configuration
466488
config = self.config_manager.load_config()

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,10 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity:
466466
@pytest_asyncio.fixture
467467
async def project_service(
468468
project_repository: ProjectRepository,
469+
file_service: FileService,
469470
) -> ProjectService:
470-
"""Create ProjectService with repository."""
471-
return ProjectService(repository=project_repository)
471+
"""Create ProjectService with repository and file service for directory operations."""
472+
return ProjectService(repository=project_repository, file_service=file_service)
472473

473474

474475
@pytest_asyncio.fixture

0 commit comments

Comments
 (0)