basic-memory 0.2.12__py3-none-any.whl → 0.16.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.

Potentially problematic release.


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

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