basic-memory 0.12.2__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (117) hide show
  1. basic_memory/__init__.py +2 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  5. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -6
  6. basic_memory/api/app.py +43 -13
  7. basic_memory/api/routers/__init__.py +4 -2
  8. basic_memory/api/routers/directory_router.py +63 -0
  9. basic_memory/api/routers/importer_router.py +152 -0
  10. basic_memory/api/routers/knowledge_router.py +139 -37
  11. basic_memory/api/routers/management_router.py +78 -0
  12. basic_memory/api/routers/memory_router.py +6 -62
  13. basic_memory/api/routers/project_router.py +234 -0
  14. basic_memory/api/routers/prompt_router.py +260 -0
  15. basic_memory/api/routers/search_router.py +3 -21
  16. basic_memory/api/routers/utils.py +130 -0
  17. basic_memory/api/template_loader.py +292 -0
  18. basic_memory/cli/app.py +20 -21
  19. basic_memory/cli/commands/__init__.py +2 -1
  20. basic_memory/cli/commands/auth.py +136 -0
  21. basic_memory/cli/commands/db.py +3 -3
  22. basic_memory/cli/commands/import_chatgpt.py +31 -207
  23. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  24. basic_memory/cli/commands/import_claude_projects.py +33 -143
  25. basic_memory/cli/commands/import_memory_json.py +26 -83
  26. basic_memory/cli/commands/mcp.py +71 -18
  27. basic_memory/cli/commands/project.py +102 -70
  28. basic_memory/cli/commands/status.py +19 -9
  29. basic_memory/cli/commands/sync.py +44 -58
  30. basic_memory/cli/commands/tool.py +6 -6
  31. basic_memory/cli/main.py +1 -5
  32. basic_memory/config.py +143 -87
  33. basic_memory/db.py +6 -4
  34. basic_memory/deps.py +227 -30
  35. basic_memory/importers/__init__.py +27 -0
  36. basic_memory/importers/base.py +79 -0
  37. basic_memory/importers/chatgpt_importer.py +222 -0
  38. basic_memory/importers/claude_conversations_importer.py +172 -0
  39. basic_memory/importers/claude_projects_importer.py +148 -0
  40. basic_memory/importers/memory_json_importer.py +93 -0
  41. basic_memory/importers/utils.py +58 -0
  42. basic_memory/markdown/entity_parser.py +5 -2
  43. basic_memory/mcp/auth_provider.py +270 -0
  44. basic_memory/mcp/external_auth_provider.py +321 -0
  45. basic_memory/mcp/project_session.py +103 -0
  46. basic_memory/mcp/prompts/__init__.py +2 -0
  47. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  48. basic_memory/mcp/prompts/recent_activity.py +20 -4
  49. basic_memory/mcp/prompts/search.py +14 -140
  50. basic_memory/mcp/prompts/sync_status.py +116 -0
  51. basic_memory/mcp/prompts/utils.py +3 -3
  52. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  53. basic_memory/mcp/server.py +86 -13
  54. basic_memory/mcp/supabase_auth_provider.py +463 -0
  55. basic_memory/mcp/tools/__init__.py +24 -0
  56. basic_memory/mcp/tools/build_context.py +43 -8
  57. basic_memory/mcp/tools/canvas.py +17 -3
  58. basic_memory/mcp/tools/delete_note.py +168 -5
  59. basic_memory/mcp/tools/edit_note.py +303 -0
  60. basic_memory/mcp/tools/list_directory.py +154 -0
  61. basic_memory/mcp/tools/move_note.py +299 -0
  62. basic_memory/mcp/tools/project_management.py +332 -0
  63. basic_memory/mcp/tools/read_content.py +15 -6
  64. basic_memory/mcp/tools/read_note.py +28 -9
  65. basic_memory/mcp/tools/recent_activity.py +47 -16
  66. basic_memory/mcp/tools/search.py +189 -8
  67. basic_memory/mcp/tools/sync_status.py +254 -0
  68. basic_memory/mcp/tools/utils.py +184 -12
  69. basic_memory/mcp/tools/view_note.py +66 -0
  70. basic_memory/mcp/tools/write_note.py +24 -17
  71. basic_memory/models/__init__.py +3 -2
  72. basic_memory/models/knowledge.py +16 -4
  73. basic_memory/models/project.py +78 -0
  74. basic_memory/models/search.py +8 -5
  75. basic_memory/repository/__init__.py +2 -0
  76. basic_memory/repository/entity_repository.py +8 -3
  77. basic_memory/repository/observation_repository.py +35 -3
  78. basic_memory/repository/project_info_repository.py +3 -2
  79. basic_memory/repository/project_repository.py +85 -0
  80. basic_memory/repository/relation_repository.py +8 -2
  81. basic_memory/repository/repository.py +107 -15
  82. basic_memory/repository/search_repository.py +192 -54
  83. basic_memory/schemas/__init__.py +6 -0
  84. basic_memory/schemas/base.py +33 -5
  85. basic_memory/schemas/directory.py +30 -0
  86. basic_memory/schemas/importer.py +34 -0
  87. basic_memory/schemas/memory.py +84 -13
  88. basic_memory/schemas/project_info.py +112 -2
  89. basic_memory/schemas/prompt.py +90 -0
  90. basic_memory/schemas/request.py +56 -2
  91. basic_memory/schemas/search.py +1 -1
  92. basic_memory/services/__init__.py +2 -1
  93. basic_memory/services/context_service.py +208 -95
  94. basic_memory/services/directory_service.py +167 -0
  95. basic_memory/services/entity_service.py +399 -6
  96. basic_memory/services/exceptions.py +6 -0
  97. basic_memory/services/file_service.py +14 -15
  98. basic_memory/services/initialization.py +170 -66
  99. basic_memory/services/link_resolver.py +35 -12
  100. basic_memory/services/migration_service.py +168 -0
  101. basic_memory/services/project_service.py +671 -0
  102. basic_memory/services/search_service.py +77 -2
  103. basic_memory/services/sync_status_service.py +181 -0
  104. basic_memory/sync/background_sync.py +25 -0
  105. basic_memory/sync/sync_service.py +102 -21
  106. basic_memory/sync/watch_service.py +63 -39
  107. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  108. basic_memory/templates/prompts/search.hbs +101 -0
  109. basic_memory/utils.py +67 -17
  110. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/METADATA +26 -4
  111. basic_memory-0.13.0.dist-info/RECORD +138 -0
  112. basic_memory/api/routers/project_info_router.py +0 -274
  113. basic_memory/mcp/main.py +0 -24
  114. basic_memory-0.12.2.dist-info/RECORD +0 -100
  115. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/WHEEL +0 -0
  116. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/entry_points.txt +0 -0
  117. {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,13 @@
1
1
  """Write note tool for Basic Memory MCP server."""
2
2
 
3
- from typing import List, Union
3
+ from typing import List, Union, Optional
4
4
 
5
5
  from loguru import logger
6
6
 
7
7
  from basic_memory.mcp.async_client import client
8
8
  from basic_memory.mcp.server import mcp
9
9
  from basic_memory.mcp.tools.utils import call_put
10
+ from basic_memory.mcp.project_session import get_active_project
10
11
  from basic_memory.schemas import EntityResponse
11
12
  from basic_memory.schemas.base import Entity
12
13
  from basic_memory.utils import parse_tags
@@ -26,6 +27,7 @@ async def write_note(
26
27
  content: str,
27
28
  folder: str,
28
29
  tags=None, # Remove type hint completely to avoid schema issues
30
+ project: Optional[str] = None,
29
31
  ) -> str:
30
32
  """Write a markdown note to the knowledge base.
31
33
 
@@ -52,9 +54,11 @@ async def write_note(
52
54
  Args:
53
55
  title: The title of the note
54
56
  content: Markdown content for the note, can include observations and relations
55
- folder: the folder where the file should be saved
57
+ folder: Folder path relative to project root where the file should be saved.
58
+ Use forward slashes (/) as separators. Examples: "notes", "projects/2025", "research/ml"
56
59
  tags: Tags to categorize the note. Can be a list of strings, a comma-separated string, or None.
57
60
  Note: If passing from external MCP clients, use a string format (e.g. "tag1,tag2,tag3")
61
+ project: Optional project name to write to. If not provided, uses current active project.
58
62
 
59
63
  Returns:
60
64
  A markdown formatted summary of the semantic content, including:
@@ -64,12 +68,19 @@ async def write_note(
64
68
  - Relation counts (resolved/unresolved)
65
69
  - Tags if present
66
70
  """
67
- logger.info("MCP tool call", tool="write_note", folder=folder, title=title, tags=tags)
71
+ logger.info(f"MCP tool call tool=write_note folder={folder}, title={title}, tags={tags}")
72
+
73
+ # Check migration status and wait briefly if needed
74
+ from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
75
+
76
+ migration_status = await wait_for_migration_or_return_status(timeout=5.0)
77
+ if migration_status: # pragma: no cover
78
+ return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before creating notes."
68
79
 
69
80
  # Process tags using the helper function
70
81
  tag_list = parse_tags(tags)
71
82
  # Create the entity request
72
- metadata = {"tags": [f"#{tag}" for tag in tag_list]} if tag_list else None
83
+ metadata = {"tags": tag_list} if tag_list else None
73
84
  entity = Entity(
74
85
  title=title,
75
86
  folder=folder,
@@ -78,10 +89,12 @@ async def write_note(
78
89
  content=content,
79
90
  entity_metadata=metadata,
80
91
  )
92
+ active_project = get_active_project(project)
93
+ project_url = active_project.project_url
81
94
 
82
95
  # Create or update via knowledge API
83
- logger.debug("Creating entity via API", permalink=entity.permalink)
84
- url = f"/knowledge/entities/{entity.permalink}"
96
+ logger.debug(f"Creating entity via API permalink={entity.permalink}")
97
+ url = f"{project_url}/knowledge/entities/{entity.permalink}"
85
98
  response = await call_put(client, url, json=entity.model_dump())
86
99
  result = EntityResponse.model_validate(response.json())
87
100
 
@@ -115,22 +128,16 @@ async def write_note(
115
128
  summary.append(f"- Resolved: {resolved}")
116
129
  if unresolved:
117
130
  summary.append(f"- Unresolved: {unresolved}")
118
- summary.append("\nUnresolved relations will be retried on next sync.")
131
+ summary.append("\nNote: Unresolved relations point to entities that don't exist yet.")
132
+ summary.append(
133
+ "They will be automatically resolved when target entities are created or during sync operations."
134
+ )
119
135
 
120
136
  if tag_list:
121
137
  summary.append(f"\n## Tags\n- {', '.join(tag_list)}")
122
138
 
123
139
  # Log the response with structured data
124
140
  logger.info(
125
- "MCP tool response",
126
- tool="write_note",
127
- action=action,
128
- permalink=result.permalink,
129
- observations_count=len(result.observations),
130
- relations_count=len(result.relations),
131
- resolved_relations=resolved,
132
- unresolved_relations=unresolved,
133
- status_code=response.status_code,
141
+ f"MCP tool response: tool=write_note action={action} permalink={result.permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved} status_code={response.status_code}"
134
142
  )
135
-
136
143
  return "\n".join(summary)
@@ -3,12 +3,13 @@
3
3
  import basic_memory
4
4
  from basic_memory.models.base import Base
5
5
  from basic_memory.models.knowledge import Entity, Observation, Relation
6
-
7
- SCHEMA_VERSION = basic_memory.__version__ + "-" + "003"
6
+ from basic_memory.models.project import Project
8
7
 
9
8
  __all__ = [
10
9
  "Base",
11
10
  "Entity",
12
11
  "Observation",
13
12
  "Relation",
13
+ "Project",
14
+ "basic_memory",
14
15
  ]
@@ -17,7 +17,6 @@ from sqlalchemy import (
17
17
  from sqlalchemy.orm import Mapped, mapped_column, relationship
18
18
 
19
19
  from basic_memory.models.base import Base
20
-
21
20
  from basic_memory.utils import generate_permalink
22
21
 
23
22
 
@@ -29,6 +28,7 @@ class Entity(Base):
29
28
  - Maps to a file on disk
30
29
  - Maintains a checksum for change detection
31
30
  - Tracks both source file and semantic properties
31
+ - Belongs to a specific project
32
32
  """
33
33
 
34
34
  __tablename__ = "entity"
@@ -38,13 +38,21 @@ class Entity(Base):
38
38
  Index("ix_entity_title", "title"),
39
39
  Index("ix_entity_created_at", "created_at"), # For timeline queries
40
40
  Index("ix_entity_updated_at", "updated_at"), # For timeline queries
41
- # Unique index only for markdown files with non-null permalinks
41
+ Index("ix_entity_project_id", "project_id"), # For project filtering
42
+ # Project-specific uniqueness constraints
42
43
  Index(
43
- "uix_entity_permalink",
44
+ "uix_entity_permalink_project",
44
45
  "permalink",
46
+ "project_id",
45
47
  unique=True,
46
48
  sqlite_where=text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
47
49
  ),
50
+ Index(
51
+ "uix_entity_file_path_project",
52
+ "file_path",
53
+ "project_id",
54
+ unique=True,
55
+ ),
48
56
  )
49
57
 
50
58
  # Core identity
@@ -54,10 +62,13 @@ class Entity(Base):
54
62
  entity_metadata: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
55
63
  content_type: Mapped[str] = mapped_column(String)
56
64
 
65
+ # Project reference
66
+ project_id: Mapped[int] = mapped_column(Integer, ForeignKey("project.id"), nullable=False)
67
+
57
68
  # Normalized path for URIs - required for markdown files only
58
69
  permalink: Mapped[Optional[str]] = mapped_column(String, nullable=True, index=True)
59
70
  # Actual filesystem relative path
60
- file_path: Mapped[str] = mapped_column(String, unique=True, index=True)
71
+ file_path: Mapped[str] = mapped_column(String, index=True)
61
72
  # checksum of file
62
73
  checksum: Mapped[Optional[str]] = mapped_column(String, nullable=True)
63
74
 
@@ -66,6 +77,7 @@ class Entity(Base):
66
77
  updated_at: Mapped[datetime] = mapped_column(DateTime)
67
78
 
68
79
  # Relationships
80
+ project = relationship("Project", back_populates="entities")
69
81
  observations = relationship(
70
82
  "Observation", back_populates="entity", cascade="all, delete-orphan"
71
83
  )
@@ -0,0 +1,78 @@
1
+ """Project model for Basic Memory."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from sqlalchemy import (
7
+ Integer,
8
+ String,
9
+ Text,
10
+ Boolean,
11
+ DateTime,
12
+ Index,
13
+ event,
14
+ )
15
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
16
+
17
+ from basic_memory.models.base import Base
18
+ from basic_memory.utils import generate_permalink
19
+
20
+
21
+ class Project(Base):
22
+ """Project model for Basic Memory.
23
+
24
+ A project represents a collection of knowledge entities that are grouped together.
25
+ Projects are stored in the app-level database and provide context for all knowledge
26
+ operations.
27
+ """
28
+
29
+ __tablename__ = "project"
30
+ __table_args__ = (
31
+ # Regular indexes
32
+ Index("ix_project_name", "name", unique=True),
33
+ Index("ix_project_permalink", "permalink", unique=True),
34
+ Index("ix_project_path", "path"),
35
+ Index("ix_project_created_at", "created_at"),
36
+ Index("ix_project_updated_at", "updated_at"),
37
+ )
38
+
39
+ # Core identity
40
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
41
+ name: Mapped[str] = mapped_column(String, unique=True)
42
+ description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
43
+
44
+ # URL-friendly identifier generated from name
45
+ permalink: Mapped[str] = mapped_column(String, unique=True)
46
+
47
+ # Filesystem path to project directory
48
+ path: Mapped[str] = mapped_column(String)
49
+
50
+ # Status flags
51
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True)
52
+ is_default: Mapped[Optional[bool]] = mapped_column(Boolean, default=None, nullable=True)
53
+
54
+ # Timestamps
55
+ created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
56
+ updated_at: Mapped[datetime] = mapped_column(
57
+ DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
58
+ )
59
+
60
+ # Define relationships to entities, observations, and relations
61
+ # These relationships will be established once we add project_id to those models
62
+ entities = relationship("Entity", back_populates="project", cascade="all, delete-orphan")
63
+
64
+ def __repr__(self) -> str: # pragma: no cover
65
+ return f"Project(id={self.id}, name='{self.name}', permalink='{self.permalink}', path='{self.path}')"
66
+
67
+
68
+ @event.listens_for(Project, "before_insert")
69
+ @event.listens_for(Project, "before_update")
70
+ def set_project_permalink(mapper, connection, project):
71
+ """Generate URL-friendly permalink for the project if needed.
72
+
73
+ This event listener ensures the permalink is always derived from the name,
74
+ even if the name changes.
75
+ """
76
+ # If the name changed or permalink is empty, regenerate permalink
77
+ if not project.permalink or project.permalink != generate_permalink(project.name):
78
+ project.permalink = generate_permalink(project.name)
@@ -13,21 +13,24 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
13
13
  permalink, -- Stable identifier (now indexed for path search)
14
14
  file_path UNINDEXED, -- Physical location
15
15
  type UNINDEXED, -- entity/relation/observation
16
-
17
- -- Relation fields
16
+
17
+ -- Project context
18
+ project_id UNINDEXED, -- Project identifier
19
+
20
+ -- Relation fields
18
21
  from_id UNINDEXED, -- Source entity
19
22
  to_id UNINDEXED, -- Target entity
20
23
  relation_type UNINDEXED, -- Type of relation
21
-
24
+
22
25
  -- Observation fields
23
26
  entity_id UNINDEXED, -- Parent entity
24
27
  category UNINDEXED, -- Observation category
25
-
28
+
26
29
  -- Common fields
27
30
  metadata UNINDEXED, -- JSON metadata
28
31
  created_at UNINDEXED, -- Creation timestamp
29
32
  updated_at UNINDEXED, -- Last update
30
-
33
+
31
34
  -- Configuration
32
35
  tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
33
36
  prefix='1,2,3,4' -- Support longer prefixes for paths
@@ -1,9 +1,11 @@
1
1
  from .entity_repository import EntityRepository
2
2
  from .observation_repository import ObservationRepository
3
+ from .project_repository import ProjectRepository
3
4
  from .relation_repository import RelationRepository
4
5
 
5
6
  __all__ = [
6
7
  "EntityRepository",
7
8
  "ObservationRepository",
9
+ "ProjectRepository",
8
10
  "RelationRepository",
9
11
  ]
@@ -18,9 +18,14 @@ class EntityRepository(Repository[Entity]):
18
18
  to strings before passing to repository methods.
19
19
  """
20
20
 
21
- def __init__(self, session_maker: async_sessionmaker[AsyncSession]):
22
- """Initialize with session maker."""
23
- super().__init__(session_maker, Entity)
21
+ def __init__(self, session_maker: async_sessionmaker[AsyncSession], project_id: int):
22
+ """Initialize with session maker and project_id filter.
23
+
24
+ Args:
25
+ session_maker: SQLAlchemy session maker
26
+ project_id: Project ID to filter all operations by
27
+ """
28
+ super().__init__(session_maker, Entity, project_id=project_id)
24
29
 
25
30
  async def get_by_permalink(self, permalink: str) -> Optional[Entity]:
26
31
  """Get entity by permalink.
@@ -1,6 +1,6 @@
1
1
  """Repository for managing Observation objects."""
2
2
 
3
- from typing import Sequence
3
+ from typing import Dict, List, Sequence
4
4
 
5
5
  from sqlalchemy import select
6
6
  from sqlalchemy.ext.asyncio import async_sessionmaker
@@ -12,8 +12,14 @@ from basic_memory.repository.repository import Repository
12
12
  class ObservationRepository(Repository[Observation]):
13
13
  """Repository for Observation model with memory-specific operations."""
14
14
 
15
- def __init__(self, session_maker: async_sessionmaker):
16
- super().__init__(session_maker, Observation)
15
+ def __init__(self, session_maker: async_sessionmaker, project_id: int):
16
+ """Initialize with session maker and project_id filter.
17
+
18
+ Args:
19
+ session_maker: SQLAlchemy session maker
20
+ project_id: Project ID to filter all operations by
21
+ """
22
+ super().__init__(session_maker, Observation, project_id=project_id)
17
23
 
18
24
  async def find_by_entity(self, entity_id: int) -> Sequence[Observation]:
19
25
  """Find all observations for a specific entity."""
@@ -38,3 +44,29 @@ class ObservationRepository(Repository[Observation]):
38
44
  query = select(Observation.category).distinct()
39
45
  result = await self.execute_query(query, use_query_options=False)
40
46
  return result.scalars().all()
47
+
48
+ async def find_by_entities(self, entity_ids: List[int]) -> Dict[int, List[Observation]]:
49
+ """Find all observations for multiple entities in a single query.
50
+
51
+ Args:
52
+ entity_ids: List of entity IDs to fetch observations for
53
+
54
+ Returns:
55
+ Dictionary mapping entity_id to list of observations
56
+ """
57
+ if not entity_ids: # pragma: no cover
58
+ return {}
59
+
60
+ # Query observations for all entities in the list
61
+ query = select(Observation).filter(Observation.entity_id.in_(entity_ids))
62
+ result = await self.execute_query(query)
63
+ observations = result.scalars().all()
64
+
65
+ # Group observations by entity_id
66
+ observations_by_entity = {}
67
+ for obs in observations:
68
+ if obs.entity_id not in observations_by_entity:
69
+ observations_by_entity[obs.entity_id] = []
70
+ observations_by_entity[obs.entity_id].append(obs)
71
+
72
+ return observations_by_entity
@@ -1,9 +1,10 @@
1
1
  from basic_memory.repository.repository import Repository
2
+ from basic_memory.models.project import Project
2
3
 
3
4
 
4
5
  class ProjectInfoRepository(Repository):
5
6
  """Repository for statistics queries."""
6
7
 
7
8
  def __init__(self, session_maker):
8
- # Initialize with a dummy model since we're just using the execute_query method
9
- super().__init__(session_maker, None) # type: ignore
9
+ # Initialize with Project model as a reference
10
+ super().__init__(session_maker, Project)
@@ -0,0 +1,85 @@
1
+ """Repository for managing projects in Basic Memory."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+
6
+ from sqlalchemy import text
7
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
8
+
9
+ from basic_memory import db
10
+ from basic_memory.models.project import Project
11
+ from basic_memory.repository.repository import Repository
12
+
13
+
14
+ class ProjectRepository(Repository[Project]):
15
+ """Repository for Project model.
16
+
17
+ Projects represent collections of knowledge entities grouped together.
18
+ Each entity, observation, and relation belongs to a specific project.
19
+ """
20
+
21
+ def __init__(self, session_maker: async_sessionmaker[AsyncSession]):
22
+ """Initialize with session maker."""
23
+ super().__init__(session_maker, Project)
24
+
25
+ async def get_by_name(self, name: str) -> Optional[Project]:
26
+ """Get project by name.
27
+
28
+ Args:
29
+ name: Unique name of the project
30
+ """
31
+ query = self.select().where(Project.name == name)
32
+ return await self.find_one(query)
33
+
34
+ async def get_by_permalink(self, permalink: str) -> Optional[Project]:
35
+ """Get project by permalink.
36
+
37
+ Args:
38
+ permalink: URL-friendly identifier for the project
39
+ """
40
+ query = self.select().where(Project.permalink == permalink)
41
+ return await self.find_one(query)
42
+
43
+ async def get_by_path(self, path: Union[Path, str]) -> Optional[Project]:
44
+ """Get project by filesystem path.
45
+
46
+ Args:
47
+ path: Path to the project directory (will be converted to string internally)
48
+ """
49
+ query = self.select().where(Project.path == str(path))
50
+ return await self.find_one(query)
51
+
52
+ async def get_default_project(self) -> Optional[Project]:
53
+ """Get the default project (the one marked as is_default=True)."""
54
+ query = self.select().where(Project.is_default.is_not(None))
55
+ return await self.find_one(query)
56
+
57
+ async def get_active_projects(self) -> Sequence[Project]:
58
+ """Get all active projects."""
59
+ query = self.select().where(Project.is_active == True) # noqa: E712
60
+ result = await self.execute_query(query)
61
+ return list(result.scalars().all())
62
+
63
+ async def set_as_default(self, project_id: int) -> Optional[Project]:
64
+ """Set a project as the default and unset previous default.
65
+
66
+ Args:
67
+ project_id: ID of the project to set as default
68
+
69
+ Returns:
70
+ The updated project if found, None otherwise
71
+ """
72
+ async with db.scoped_session(self.session_maker) as session:
73
+ # First, clear the default flag for all projects using direct SQL
74
+ await session.execute(
75
+ text("UPDATE project SET is_default = NULL WHERE is_default IS NOT NULL")
76
+ )
77
+ await session.flush()
78
+
79
+ # Set the new default project
80
+ target_project = await self.select_by_id(session, project_id)
81
+ if target_project:
82
+ target_project.is_default = True
83
+ await session.flush()
84
+ return target_project
85
+ return None # pragma: no cover
@@ -16,8 +16,14 @@ from basic_memory.repository.repository import Repository
16
16
  class RelationRepository(Repository[Relation]):
17
17
  """Repository for Relation model with memory-specific operations."""
18
18
 
19
- def __init__(self, session_maker: async_sessionmaker):
20
- super().__init__(session_maker, Relation)
19
+ def __init__(self, session_maker: async_sessionmaker, project_id: int):
20
+ """Initialize with session maker and project_id filter.
21
+
22
+ Args:
23
+ session_maker: SQLAlchemy session maker
24
+ project_id: Project ID to filter all operations by
25
+ """
26
+ super().__init__(session_maker, Relation, project_id=project_id)
21
27
 
22
28
  async def find_relation(
23
29
  self, from_permalink: str, to_permalink: str, relation_type: str