basic-memory 0.12.3__py3-none-any.whl → 0.13.0b1__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 (107) hide show
  1. basic_memory/__init__.py +7 -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/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +145 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b1.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,172 @@
1
+ """Claude conversations import service for Basic Memory."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List
7
+
8
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
9
+ from basic_memory.importers.base import Importer
10
+ from basic_memory.schemas.importer import ChatImportResult
11
+ from basic_memory.importers.utils import clean_filename, format_timestamp
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ClaudeConversationsImporter(Importer[ChatImportResult]):
17
+ """Service for importing Claude conversations."""
18
+
19
+ async def import_data(
20
+ self, source_data, destination_folder: str, **kwargs: Any
21
+ ) -> ChatImportResult:
22
+ """Import conversations from Claude JSON export.
23
+
24
+ Args:
25
+ source_data: Path to the Claude conversations.json file.
26
+ destination_folder: Destination folder within the project.
27
+ **kwargs: Additional keyword arguments.
28
+
29
+ Returns:
30
+ ChatImportResult containing statistics and status of the import.
31
+ """
32
+ try:
33
+ # Ensure the destination folder exists
34
+ folder_path = self.ensure_folder_exists(destination_folder)
35
+
36
+ conversations = source_data
37
+
38
+ # Process each conversation
39
+ messages_imported = 0
40
+ chats_imported = 0
41
+
42
+ for chat in conversations:
43
+ # Convert to entity
44
+ entity = self._format_chat_content(
45
+ base_path=folder_path,
46
+ name=chat["name"],
47
+ messages=chat["chat_messages"],
48
+ created_at=chat["created_at"],
49
+ modified_at=chat["updated_at"],
50
+ )
51
+
52
+ # Write file
53
+ file_path = self.base_path / Path(f"{entity.frontmatter.metadata['permalink']}.md")
54
+ await self.write_entity(entity, file_path)
55
+
56
+ chats_imported += 1
57
+ messages_imported += len(chat["chat_messages"])
58
+
59
+ return ChatImportResult(
60
+ import_count={"conversations": chats_imported, "messages": messages_imported},
61
+ success=True,
62
+ conversations=chats_imported,
63
+ messages=messages_imported,
64
+ )
65
+
66
+ except Exception as e: # pragma: no cover
67
+ logger.exception("Failed to import Claude conversations")
68
+ return self.handle_error("Failed to import Claude conversations", e) # pyright: ignore [reportReturnType]
69
+
70
+ def _format_chat_content(
71
+ self,
72
+ base_path: Path,
73
+ name: str,
74
+ messages: List[Dict[str, Any]],
75
+ created_at: str,
76
+ modified_at: str,
77
+ ) -> EntityMarkdown:
78
+ """Convert chat messages to Basic Memory entity format.
79
+
80
+ Args:
81
+ base_path: Base path for the entity.
82
+ name: Chat name.
83
+ messages: List of chat messages.
84
+ created_at: Creation timestamp.
85
+ modified_at: Modification timestamp.
86
+
87
+ Returns:
88
+ EntityMarkdown instance representing the conversation.
89
+ """
90
+ # Generate permalink
91
+ date_prefix = datetime.fromisoformat(created_at.replace("Z", "+00:00")).strftime("%Y%m%d")
92
+ clean_title = clean_filename(name)
93
+ permalink = f"{base_path.name}/{date_prefix}-{clean_title}"
94
+
95
+ # Format content
96
+ content = self._format_chat_markdown(
97
+ name=name,
98
+ messages=messages,
99
+ created_at=created_at,
100
+ modified_at=modified_at,
101
+ permalink=permalink,
102
+ )
103
+
104
+ # Create entity
105
+ entity = EntityMarkdown(
106
+ frontmatter=EntityFrontmatter(
107
+ metadata={
108
+ "type": "conversation",
109
+ "title": name,
110
+ "created": created_at,
111
+ "modified": modified_at,
112
+ "permalink": permalink,
113
+ }
114
+ ),
115
+ content=content,
116
+ )
117
+
118
+ return entity
119
+
120
+ def _format_chat_markdown(
121
+ self,
122
+ name: str,
123
+ messages: List[Dict[str, Any]],
124
+ created_at: str,
125
+ modified_at: str,
126
+ permalink: str,
127
+ ) -> str:
128
+ """Format chat as clean markdown.
129
+
130
+ Args:
131
+ name: Chat name.
132
+ messages: List of chat messages.
133
+ created_at: Creation timestamp.
134
+ modified_at: Modification timestamp.
135
+ permalink: Permalink for the entity.
136
+
137
+ Returns:
138
+ Formatted markdown content.
139
+ """
140
+ # Start with frontmatter and title
141
+ lines = [
142
+ f"# {name}\n",
143
+ ]
144
+
145
+ # Add messages
146
+ for msg in messages:
147
+ # Format timestamp
148
+ ts = format_timestamp(msg["created_at"])
149
+
150
+ # Add message header
151
+ lines.append(f"### {msg['sender'].title()} ({ts})")
152
+
153
+ # Handle message content
154
+ content = msg.get("text", "")
155
+ if msg.get("content"):
156
+ content = " ".join(c.get("text", "") for c in msg["content"])
157
+ lines.append(content)
158
+
159
+ # Handle attachments
160
+ attachments = msg.get("attachments", [])
161
+ for attachment in attachments:
162
+ if "file_name" in attachment:
163
+ lines.append(f"\n**Attachment: {attachment['file_name']}**")
164
+ if "extracted_content" in attachment:
165
+ lines.append("```")
166
+ lines.append(attachment["extracted_content"])
167
+ lines.append("```")
168
+
169
+ # Add spacing between messages
170
+ lines.append("")
171
+
172
+ return "\n".join(lines)
@@ -0,0 +1,148 @@
1
+ """Claude projects import service for Basic Memory."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+
6
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown
7
+ from basic_memory.importers.base import Importer
8
+ from basic_memory.schemas.importer import ProjectImportResult
9
+ from basic_memory.importers.utils import clean_filename
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ClaudeProjectsImporter(Importer[ProjectImportResult]):
15
+ """Service for importing Claude projects."""
16
+
17
+ async def import_data(
18
+ self, source_data, destination_folder: str, **kwargs: Any
19
+ ) -> ProjectImportResult:
20
+ """Import projects from Claude JSON export.
21
+
22
+ Args:
23
+ source_path: Path to the Claude projects.json file.
24
+ destination_folder: Base folder for projects within the project.
25
+ **kwargs: Additional keyword arguments.
26
+
27
+ Returns:
28
+ ProjectImportResult containing statistics and status of the import.
29
+ """
30
+ try:
31
+ # Ensure the base folder exists
32
+ base_path = self.base_path
33
+ if destination_folder:
34
+ base_path = self.ensure_folder_exists(destination_folder)
35
+
36
+ projects = source_data
37
+
38
+ # Process each project
39
+ docs_imported = 0
40
+ prompts_imported = 0
41
+
42
+ for project in projects:
43
+ project_dir = clean_filename(project["name"])
44
+
45
+ # Create project directories
46
+ docs_dir = base_path / project_dir / "docs"
47
+ docs_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ # Import prompt template if it exists
50
+ if prompt_entity := self._format_prompt_markdown(project):
51
+ file_path = base_path / f"{prompt_entity.frontmatter.metadata['permalink']}.md"
52
+ await self.write_entity(prompt_entity, file_path)
53
+ prompts_imported += 1
54
+
55
+ # Import project documents
56
+ for doc in project.get("docs", []):
57
+ entity = self._format_project_markdown(project, doc)
58
+ file_path = base_path / f"{entity.frontmatter.metadata['permalink']}.md"
59
+ await self.write_entity(entity, file_path)
60
+ docs_imported += 1
61
+
62
+ return ProjectImportResult(
63
+ import_count={"documents": docs_imported, "prompts": prompts_imported},
64
+ success=True,
65
+ documents=docs_imported,
66
+ prompts=prompts_imported,
67
+ )
68
+
69
+ except Exception as e: # pragma: no cover
70
+ logger.exception("Failed to import Claude projects")
71
+ return self.handle_error("Failed to import Claude projects", e) # pyright: ignore [reportReturnType]
72
+
73
+ def _format_project_markdown(
74
+ self, project: Dict[str, Any], doc: Dict[str, Any]
75
+ ) -> EntityMarkdown:
76
+ """Format a project document as a Basic Memory entity.
77
+
78
+ Args:
79
+ project: Project data.
80
+ doc: Document data.
81
+
82
+ Returns:
83
+ EntityMarkdown instance representing the document.
84
+ """
85
+ # Extract timestamps
86
+ created_at = doc.get("created_at") or project["created_at"]
87
+ modified_at = project["updated_at"]
88
+
89
+ # Generate clean names for organization
90
+ project_dir = clean_filename(project["name"])
91
+ doc_file = clean_filename(doc["filename"])
92
+
93
+ # Create entity
94
+ entity = EntityMarkdown(
95
+ frontmatter=EntityFrontmatter(
96
+ metadata={
97
+ "type": "project_doc",
98
+ "title": doc["filename"],
99
+ "created": created_at,
100
+ "modified": modified_at,
101
+ "permalink": f"{project_dir}/docs/{doc_file}",
102
+ "project_name": project["name"],
103
+ "project_uuid": project["uuid"],
104
+ "doc_uuid": doc["uuid"],
105
+ }
106
+ ),
107
+ content=doc["content"],
108
+ )
109
+
110
+ return entity
111
+
112
+ def _format_prompt_markdown(self, project: Dict[str, Any]) -> Optional[EntityMarkdown]:
113
+ """Format project prompt template as a Basic Memory entity.
114
+
115
+ Args:
116
+ project: Project data.
117
+
118
+ Returns:
119
+ EntityMarkdown instance representing the prompt template, or None if
120
+ no prompt template exists.
121
+ """
122
+ if not project.get("prompt_template"):
123
+ return None
124
+
125
+ # Extract timestamps
126
+ created_at = project["created_at"]
127
+ modified_at = project["updated_at"]
128
+
129
+ # Generate clean project directory name
130
+ project_dir = clean_filename(project["name"])
131
+
132
+ # Create entity
133
+ entity = EntityMarkdown(
134
+ frontmatter=EntityFrontmatter(
135
+ metadata={
136
+ "type": "prompt_template",
137
+ "title": f"Prompt Template: {project['name']}",
138
+ "created": created_at,
139
+ "modified": modified_at,
140
+ "permalink": f"{project_dir}/prompt-template",
141
+ "project_name": project["name"],
142
+ "project_uuid": project["uuid"],
143
+ }
144
+ ),
145
+ content=f"# Prompt Template: {project['name']}\n\n{project['prompt_template']}",
146
+ )
147
+
148
+ return entity
@@ -0,0 +1,93 @@
1
+ """Memory JSON import service for Basic Memory."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List
5
+
6
+ from basic_memory.config import config
7
+ from basic_memory.markdown.schemas import EntityFrontmatter, EntityMarkdown, Observation, Relation
8
+ from basic_memory.importers.base import Importer
9
+ from basic_memory.schemas.importer import EntityImportResult
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class MemoryJsonImporter(Importer[EntityImportResult]):
15
+ """Service for importing memory.json format data."""
16
+
17
+ async def import_data(
18
+ self, source_data, destination_folder: str = "", **kwargs: Any
19
+ ) -> EntityImportResult:
20
+ """Import entities and relations from a memory.json file.
21
+
22
+ Args:
23
+ source_data: Path to the memory.json file.
24
+ destination_folder: Optional destination folder within the project.
25
+ **kwargs: Additional keyword arguments.
26
+
27
+ Returns:
28
+ EntityImportResult containing statistics and status of the import.
29
+ """
30
+ try:
31
+ # First pass - collect all relations by source entity
32
+ entity_relations: Dict[str, List[Relation]] = {}
33
+ entities: Dict[str, Dict[str, Any]] = {}
34
+
35
+ # Ensure the base path exists
36
+ base_path = config.home # pragma: no cover
37
+ if destination_folder: # pragma: no cover
38
+ base_path = self.ensure_folder_exists(destination_folder)
39
+
40
+ # First pass - collect entities and relations
41
+ for line in source_data:
42
+ data = line
43
+ if data["type"] == "entity":
44
+ entities[data["name"]] = data
45
+ elif data["type"] == "relation":
46
+ # Store relation with its source entity
47
+ source = data.get("from") or data.get("from_id")
48
+ if source not in entity_relations:
49
+ entity_relations[source] = []
50
+ entity_relations[source].append(
51
+ Relation(
52
+ type=data.get("relationType") or data.get("relation_type"),
53
+ target=data.get("to") or data.get("to_id"),
54
+ )
55
+ )
56
+
57
+ # Second pass - create and write entities
58
+ entities_created = 0
59
+ for name, entity_data in entities.items():
60
+ # Ensure entity type directory exists
61
+ entity_type_dir = base_path / entity_data["entityType"]
62
+ entity_type_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ entity = EntityMarkdown(
65
+ frontmatter=EntityFrontmatter(
66
+ metadata={
67
+ "type": entity_data["entityType"],
68
+ "title": name,
69
+ "permalink": f"{entity_data['entityType']}/{name}",
70
+ }
71
+ ),
72
+ content=f"# {name}\n",
73
+ observations=[Observation(content=obs) for obs in entity_data["observations"]],
74
+ relations=entity_relations.get(name, []),
75
+ )
76
+
77
+ # Write entity file
78
+ file_path = base_path / f"{entity_data['entityType']}/{name}.md"
79
+ await self.write_entity(entity, file_path)
80
+ entities_created += 1
81
+
82
+ relations_count = sum(len(rels) for rels in entity_relations.values())
83
+
84
+ return EntityImportResult(
85
+ import_count={"entities": entities_created, "relations": relations_count},
86
+ success=True,
87
+ entities=entities_created,
88
+ relations=relations_count,
89
+ )
90
+
91
+ except Exception as e: # pragma: no cover
92
+ logger.exception("Failed to import memory.json")
93
+ return self.handle_error("Failed to import memory.json", e) # pyright: ignore [reportReturnType]
@@ -0,0 +1,58 @@
1
+ """Utility functions for import services."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ def clean_filename(name: str) -> str: # pragma: no cover
9
+ """Clean a string to be used as a filename.
10
+
11
+ Args:
12
+ name: The string to clean.
13
+
14
+ Returns:
15
+ A cleaned string suitable for use as a filename.
16
+ """
17
+ # Replace common punctuation and whitespace with underscores
18
+ name = re.sub(r"[\s\-,.:/\\\[\]\(\)]+", "_", name)
19
+ # Remove any non-alphanumeric or underscore characters
20
+ name = re.sub(r"[^\w]+", "", name)
21
+ # Ensure the name isn't too long
22
+ if len(name) > 100: # pragma: no cover
23
+ name = name[:100]
24
+ # Ensure the name isn't empty
25
+ if not name: # pragma: no cover
26
+ name = "untitled"
27
+ return name
28
+
29
+
30
+ def format_timestamp(timestamp: Any) -> str: # pragma: no cover
31
+ """Format a timestamp for use in a filename or title.
32
+
33
+ Args:
34
+ timestamp: A timestamp in various formats.
35
+
36
+ Returns:
37
+ A formatted string representation of the timestamp.
38
+ """
39
+ if isinstance(timestamp, str):
40
+ try:
41
+ # Try ISO format
42
+ timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
43
+ except ValueError:
44
+ try:
45
+ # Try unix timestamp as string
46
+ timestamp = datetime.fromtimestamp(float(timestamp))
47
+ except ValueError:
48
+ # Return as is if we can't parse it
49
+ return timestamp
50
+ elif isinstance(timestamp, (int, float)):
51
+ # Unix timestamp
52
+ timestamp = datetime.fromtimestamp(timestamp)
53
+
54
+ if isinstance(timestamp, datetime):
55
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
56
+
57
+ # Return as is if we can't format it
58
+ return str(timestamp) # pragma: no cover
@@ -92,7 +92,6 @@ class EntityParser:
92
92
  async def parse_file(self, path: Path | str) -> EntityMarkdown:
93
93
  """Parse markdown file into EntityMarkdown."""
94
94
 
95
- # TODO move to api endpoint to check if absolute path was requested
96
95
  # Check if the path is already absolute
97
96
  if (
98
97
  isinstance(path, Path)
@@ -101,12 +100,16 @@ class EntityParser:
101
100
  ):
102
101
  absolute_path = Path(path)
103
102
  else:
104
- absolute_path = self.base_path / path
103
+ absolute_path = self.get_file_path(path)
105
104
 
106
105
  # Parse frontmatter and content using python-frontmatter
107
106
  file_content = absolute_path.read_text(encoding="utf-8")
108
107
  return await self.parse_file_content(absolute_path, file_content)
109
108
 
109
+ def get_file_path(self, path):
110
+ """Get absolute path for a file using the base path for the project."""
111
+ return self.base_path / path
112
+
110
113
  async def parse_file_content(self, absolute_path, file_content):
111
114
  post = frontmatter.loads(file_content)
112
115
  # Extract file stat info