basic-memory 0.17.1__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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,308 @@
1
+ """Directory service for managing file directories and tree structure."""
2
+
3
+ import fnmatch
4
+ import logging
5
+ import os
6
+ from datetime import datetime
7
+ from typing import Dict, List, Optional, Sequence
8
+
9
+
10
+ from basic_memory.models import Entity
11
+ from basic_memory.repository import EntityRepository
12
+ from basic_memory.schemas.directory import DirectoryNode
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _mtime_to_datetime(entity: Entity) -> datetime:
18
+ """Convert entity mtime (file modification time) to datetime.
19
+
20
+ Returns the file's actual modification time, falling back to updated_at
21
+ if mtime is not available.
22
+ """
23
+ if entity.mtime:
24
+ return datetime.fromtimestamp(entity.mtime).astimezone()
25
+ return entity.updated_at
26
+
27
+
28
+ class DirectoryService:
29
+ """Service for working with directory trees."""
30
+
31
+ def __init__(self, entity_repository: EntityRepository):
32
+ """Initialize the directory service.
33
+
34
+ Args:
35
+ entity_repository: Directory repository for data access.
36
+ """
37
+ self.entity_repository = entity_repository
38
+
39
+ async def get_directory_tree(self) -> DirectoryNode:
40
+ """Build a hierarchical directory tree from indexed files."""
41
+
42
+ # Get all files from DB (flat list)
43
+ entity_rows = await self.entity_repository.find_all()
44
+
45
+ # Create a root directory node
46
+ root_node = DirectoryNode(name="Root", directory_path="/", type="directory")
47
+
48
+ # Map to store directory nodes by path for easy lookup
49
+ dir_map: Dict[str, DirectoryNode] = {root_node.directory_path: root_node}
50
+
51
+ # First pass: create all directory nodes
52
+ for file in entity_rows:
53
+ # Process directory path components
54
+ parts = [p for p in file.file_path.split("/") if p]
55
+
56
+ # Create directory structure
57
+ current_path = "/"
58
+ for i, part in enumerate(parts[:-1]): # Skip the filename
59
+ parent_path = current_path
60
+ # Build the directory path
61
+ current_path = (
62
+ f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
63
+ )
64
+
65
+ # Create directory node if it doesn't exist
66
+ if current_path not in dir_map:
67
+ dir_node = DirectoryNode(
68
+ name=part, directory_path=current_path, type="directory"
69
+ )
70
+ dir_map[current_path] = dir_node
71
+
72
+ # Add to parent's children
73
+ if parent_path in dir_map:
74
+ dir_map[parent_path].children.append(dir_node)
75
+
76
+ # Second pass: add file nodes to their parent directories
77
+ for file in entity_rows:
78
+ file_name = os.path.basename(file.file_path)
79
+ parent_dir = os.path.dirname(file.file_path)
80
+ directory_path = "/" if parent_dir == "" else f"/{parent_dir}"
81
+
82
+ # Create file node
83
+ file_node = DirectoryNode(
84
+ name=file_name,
85
+ file_path=file.file_path, # Original path from DB (no leading slash)
86
+ directory_path=f"/{file.file_path}", # Path with leading slash
87
+ type="file",
88
+ title=file.title,
89
+ permalink=file.permalink,
90
+ entity_id=file.id,
91
+ entity_type=file.entity_type,
92
+ content_type=file.content_type,
93
+ updated_at=_mtime_to_datetime(file),
94
+ )
95
+
96
+ # Add to parent directory's children
97
+ if directory_path in dir_map:
98
+ dir_map[directory_path].children.append(file_node)
99
+ else:
100
+ # If parent directory doesn't exist (should be rare), add to root
101
+ dir_map["/"].children.append(file_node) # pragma: no cover
102
+
103
+ # Return the root node with its children
104
+ return root_node
105
+
106
+ async def get_directory_structure(self) -> DirectoryNode:
107
+ """Build a hierarchical directory structure without file details.
108
+
109
+ Optimized method for folder navigation that only returns directory nodes,
110
+ no file metadata. Much faster than get_directory_tree() for large knowledge bases.
111
+
112
+ Returns:
113
+ DirectoryNode tree containing only folders (type="directory")
114
+ """
115
+ # Get unique directories without loading entities
116
+ directories = await self.entity_repository.get_distinct_directories()
117
+
118
+ # Create a root directory node
119
+ root_node = DirectoryNode(name="Root", directory_path="/", type="directory")
120
+
121
+ # Map to store directory nodes by path for easy lookup
122
+ dir_map: Dict[str, DirectoryNode] = {"/": root_node}
123
+
124
+ # Build tree with just folders
125
+ for dir_path in directories:
126
+ parts = [p for p in dir_path.split("/") if p]
127
+ current_path = "/"
128
+
129
+ for i, part in enumerate(parts):
130
+ parent_path = current_path
131
+ # Build the directory path
132
+ current_path = (
133
+ f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
134
+ )
135
+
136
+ # Create directory node if it doesn't exist
137
+ if current_path not in dir_map:
138
+ dir_node = DirectoryNode(
139
+ name=part, directory_path=current_path, type="directory"
140
+ )
141
+ dir_map[current_path] = dir_node
142
+
143
+ # Add to parent's children
144
+ if parent_path in dir_map:
145
+ dir_map[parent_path].children.append(dir_node)
146
+
147
+ return root_node
148
+
149
+ async def list_directory(
150
+ self,
151
+ dir_name: str = "/",
152
+ depth: int = 1,
153
+ file_name_glob: Optional[str] = None,
154
+ ) -> List[DirectoryNode]:
155
+ """List directory contents with filtering and depth control.
156
+
157
+ Args:
158
+ dir_name: Directory path to list (default: root "/")
159
+ depth: Recursion depth (1 = immediate children only)
160
+ file_name_glob: Glob pattern for filtering file names
161
+
162
+ Returns:
163
+ List of DirectoryNode objects matching the criteria
164
+ """
165
+ # Normalize directory path
166
+ # Strip ./ prefix if present (handles relative path notation)
167
+ if dir_name.startswith("./"):
168
+ dir_name = dir_name[2:] # Remove "./" prefix
169
+
170
+ # Ensure path starts with "/"
171
+ if not dir_name.startswith("/"):
172
+ dir_name = f"/{dir_name}"
173
+
174
+ # Remove trailing slashes except for root
175
+ if dir_name != "/" and dir_name.endswith("/"):
176
+ dir_name = dir_name.rstrip("/")
177
+
178
+ # Optimize: Query only entities in the target directory
179
+ # instead of loading the entire tree
180
+ dir_prefix = dir_name.lstrip("/")
181
+ entity_rows = await self.entity_repository.find_by_directory_prefix(dir_prefix)
182
+
183
+ # Build a partial tree from only the relevant entities
184
+ root_tree = self._build_directory_tree_from_entities(entity_rows, dir_name)
185
+
186
+ # Find the target directory node
187
+ target_node = self._find_directory_node(root_tree, dir_name)
188
+ if not target_node:
189
+ return []
190
+
191
+ # Collect nodes with depth and glob filtering
192
+ result = []
193
+ self._collect_nodes_recursive(target_node, result, depth, file_name_glob, 0)
194
+
195
+ return result
196
+
197
+ def _build_directory_tree_from_entities(
198
+ self, entity_rows: Sequence[Entity], root_path: str
199
+ ) -> DirectoryNode:
200
+ """Build a directory tree from a subset of entities.
201
+
202
+ Args:
203
+ entity_rows: Sequence of entity objects to build tree from
204
+ root_path: Root directory path for the tree
205
+
206
+ Returns:
207
+ DirectoryNode representing the tree root
208
+ """
209
+ # Create a root directory node
210
+ root_node = DirectoryNode(name="Root", directory_path=root_path, type="directory")
211
+
212
+ # Map to store directory nodes by path for easy lookup
213
+ dir_map: Dict[str, DirectoryNode] = {root_path: root_node}
214
+
215
+ # First pass: create all directory nodes
216
+ for file in entity_rows:
217
+ # Process directory path components
218
+ parts = [p for p in file.file_path.split("/") if p]
219
+
220
+ # Create directory structure
221
+ current_path = "/"
222
+ for i, part in enumerate(parts[:-1]): # Skip the filename
223
+ parent_path = current_path
224
+ # Build the directory path
225
+ current_path = (
226
+ f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
227
+ )
228
+
229
+ # Create directory node if it doesn't exist
230
+ if current_path not in dir_map:
231
+ dir_node = DirectoryNode(
232
+ name=part, directory_path=current_path, type="directory"
233
+ )
234
+ dir_map[current_path] = dir_node
235
+
236
+ # Add to parent's children
237
+ if parent_path in dir_map:
238
+ dir_map[parent_path].children.append(dir_node)
239
+
240
+ # Second pass: add file nodes to their parent directories
241
+ for file in entity_rows:
242
+ file_name = os.path.basename(file.file_path)
243
+ parent_dir = os.path.dirname(file.file_path)
244
+ directory_path = "/" if parent_dir == "" else f"/{parent_dir}"
245
+
246
+ # Create file node
247
+ file_node = DirectoryNode(
248
+ name=file_name,
249
+ file_path=file.file_path,
250
+ directory_path=f"/{file.file_path}",
251
+ type="file",
252
+ title=file.title,
253
+ permalink=file.permalink,
254
+ entity_id=file.id,
255
+ entity_type=file.entity_type,
256
+ content_type=file.content_type,
257
+ updated_at=_mtime_to_datetime(file),
258
+ )
259
+
260
+ # Add to parent directory's children
261
+ if directory_path in dir_map:
262
+ dir_map[directory_path].children.append(file_node)
263
+ elif root_path in dir_map:
264
+ # Fallback to root if parent not found
265
+ dir_map[root_path].children.append(file_node)
266
+
267
+ return root_node
268
+
269
+ def _find_directory_node(
270
+ self, root: DirectoryNode, target_path: str
271
+ ) -> Optional[DirectoryNode]:
272
+ """Find a directory node by path in the tree."""
273
+ if root.directory_path == target_path:
274
+ return root
275
+
276
+ for child in root.children:
277
+ if child.type == "directory":
278
+ found = self._find_directory_node(child, target_path)
279
+ if found:
280
+ return found
281
+
282
+ return None
283
+
284
+ def _collect_nodes_recursive(
285
+ self,
286
+ node: DirectoryNode,
287
+ result: List[DirectoryNode],
288
+ max_depth: int,
289
+ file_name_glob: Optional[str],
290
+ current_depth: int,
291
+ ) -> None:
292
+ """Recursively collect nodes with depth and glob filtering."""
293
+ if current_depth >= max_depth:
294
+ return
295
+
296
+ for child in node.children:
297
+ # Apply glob filtering
298
+ if file_name_glob and not fnmatch.fnmatch(child.name, file_name_glob):
299
+ continue
300
+
301
+ # Add the child to results
302
+ result.append(child)
303
+
304
+ # Recurse into subdirectories if we haven't reached max depth
305
+ if child.type == "directory" and current_depth < max_depth:
306
+ self._collect_nodes_recursive(
307
+ child, result, max_depth, file_name_glob, current_depth + 1
308
+ )